@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
@@ -12,11 +12,10 @@ import {
12
12
  } from './errors.ts'
13
13
  import { runProgressStep, type StepProgressReporter } from './reporter.ts'
14
14
 
15
- const BOOTSTRAP_DIRECTORY = path.resolve(
16
- path.dirname(fileURLToPath(import.meta.url)),
17
- '../../bootstrap',
18
- )
19
- const BOOTSTRAP_EXCLUDED_NAMES = new Set(['.gitkeep', 'node_modules'])
15
+ const MODULE_DIRECTORY = path.dirname(fileURLToPath(import.meta.url))
16
+ const PACKAGE_TEMPLATE_DIRECTORY = path.resolve(MODULE_DIRECTORY, '../../template')
17
+ const REPO_TEMPLATE_DIRECTORY = path.resolve(MODULE_DIRECTORY, '../../../../template')
18
+ const TEMPLATE_EXCLUDED_NAMES = new Set(['.gitkeep', 'node_modules'])
20
19
  export const MINIMUM_SUPPORTED_NODE_VERSION = '24.3.0'
21
20
 
22
21
  export interface BootstrapProjectOptions {
@@ -67,9 +66,9 @@ export async function bootstrapProject(
67
66
  await runProgressStep(progress, 'prepare-target-directory', () =>
68
67
  ensureTargetDirectory(targetDir, options.force),
69
68
  )
70
- await runProgressStep(progress, 'generate-scaffold-files', () =>
71
- copyBootstrapDirectory({
72
- sourceDir: BOOTSTRAP_DIRECTORY,
69
+ await runProgressStep(progress, 'generate-scaffold-files', async () =>
70
+ copyTemplateDirectory({
71
+ sourceDir: await resolveTemplateDirectory(),
73
72
  targetDir,
74
73
  templateValues: createTemplateValues(config),
75
74
  }),
@@ -105,6 +104,34 @@ function createTemplateValues(config: BootstrapConfig): TemplateValues {
105
104
  }
106
105
  }
107
106
 
107
+ async function resolveTemplateDirectory(): Promise<string> {
108
+ if (await isDirectory(PACKAGE_TEMPLATE_DIRECTORY)) {
109
+ return PACKAGE_TEMPLATE_DIRECTORY
110
+ }
111
+
112
+ if (await isDirectory(REPO_TEMPLATE_DIRECTORY)) {
113
+ return REPO_TEMPLATE_DIRECTORY
114
+ }
115
+
116
+ throw new Error(
117
+ `Could not find the Remix app template at ${PACKAGE_TEMPLATE_DIRECTORY} or ${REPO_TEMPLATE_DIRECTORY}.`,
118
+ )
119
+ }
120
+
121
+ async function isDirectory(directory: string): Promise<boolean> {
122
+ try {
123
+ let stats = await fs.stat(directory)
124
+ return stats.isDirectory()
125
+ } catch (error) {
126
+ let nodeError = error as NodeJS.ErrnoException
127
+ if (nodeError.code === 'ENOENT') {
128
+ return false
129
+ }
130
+
131
+ throw error
132
+ }
133
+ }
134
+
108
135
  async function writeScaffoldPackageJson(targetDir: string, config: BootstrapConfig): Promise<void> {
109
136
  let packageJsonPath = path.join(targetDir, 'package.json')
110
137
  let packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as {
@@ -118,7 +145,6 @@ async function writeScaffoldPackageJson(targetDir: string, config: BootstrapConf
118
145
  packageJson.dependencies = {
119
146
  ...packageJson.dependencies,
120
147
  remix: config.remixVersion,
121
- tsx: 'latest',
122
148
  }
123
149
  packageJson.devDependencies = {
124
150
  ...packageJson.devDependencies,
@@ -154,7 +180,7 @@ async function ensureTargetDirectory(targetDir: string, force: boolean): Promise
154
180
  await fs.mkdir(targetDir, { recursive: true })
155
181
  }
156
182
 
157
- async function copyBootstrapDirectory({
183
+ async function copyTemplateDirectory({
158
184
  sourceDir,
159
185
  targetDir,
160
186
  templateValues,
@@ -167,15 +193,15 @@ async function copyBootstrapDirectory({
167
193
 
168
194
  for (let entry of entries) {
169
195
  let sourcePath = path.join(sourceDir, entry.name)
170
- let targetPath = path.join(targetDir, entry.name)
196
+ let targetPath = path.join(targetDir, entry.name === 'gitignore' ? '.gitignore' : entry.name)
171
197
 
172
- if (BOOTSTRAP_EXCLUDED_NAMES.has(entry.name)) {
198
+ if (TEMPLATE_EXCLUDED_NAMES.has(entry.name)) {
173
199
  continue
174
200
  }
175
201
 
176
202
  if (entry.isDirectory()) {
177
203
  await fs.mkdir(targetPath, { recursive: true })
178
- await copyBootstrapDirectory({ sourceDir: sourcePath, targetDir: targetPath, templateValues })
204
+ await copyTemplateDirectory({ sourceDir: sourcePath, targetDir: targetPath, templateValues })
179
205
  continue
180
206
  }
181
207
 
package/src/lib/cli.ts CHANGED
@@ -5,11 +5,40 @@ import { resolveCliContext, type CliContext } from './cli-context.ts'
5
5
  import { renderCliError, unknownCommand } from './errors.ts'
6
6
  import { configureColors, restoreTerminalFormatting } from './terminal.ts'
7
7
 
8
+ /**
9
+ * Options accepted by {@link runRemix}.
10
+ */
8
11
  export interface RunRemixOptions {
12
+ /**
13
+ * Directory used as the working root when resolving project files (default
14
+ * `process.cwd()`).
15
+ */
9
16
  cwd?: string
17
+ /**
18
+ * Override for the Remix version reported by `remix --version`. Defaults to
19
+ * the version of the installed `@remix-run/cli` package.
20
+ */
10
21
  remixVersion?: string
11
22
  }
12
23
 
24
+ /**
25
+ * Entry point for the `remix` CLI. Parses `argv`, dispatches to the matching
26
+ * subcommand (`new`, `doctor`, `routes`, `test`, `version`, `completion`,
27
+ * `help`), and resolves with the exit code the process should use.
28
+ *
29
+ * @param argv Argument vector to parse, excluding the node and script paths
30
+ * (default `process.argv.slice(2)`).
31
+ * @param options Overrides for CLI context resolution.
32
+ * @returns The numeric exit code (`0` on success, non-zero on failure).
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { runRemix } from '@remix-run/cli'
37
+ *
38
+ * let exitCode = await runRemix(process.argv.slice(2))
39
+ * process.exit(exitCode)
40
+ * ```
41
+ */
13
42
  export async function runRemix(
14
43
  argv: string[] = process.argv.slice(2),
15
44
  options: RunRemixOptions = {},
@@ -17,12 +46,10 @@ export async function runRemix(
17
46
  let context = await resolveCliContext(options)
18
47
 
19
48
  try {
20
- while (argv[0] === '--') {
21
- argv = argv.slice(1)
22
- }
49
+ argv = stripLeadingSeparators(argv)
23
50
 
24
51
  let globalOptions = extractGlobalOptions(argv)
25
- argv = globalOptions.argv
52
+ argv = stripLeadingSeparators(globalOptions.argv)
26
53
  configureColors({ disabled: globalOptions.noColor })
27
54
 
28
55
  if (argv.length === 0) {
@@ -43,6 +70,14 @@ export async function runRemix(
43
70
  }
44
71
  }
45
72
 
73
+ function stripLeadingSeparators(argv: string[]): string[] {
74
+ while (argv[0] === '--') {
75
+ argv = argv.slice(1)
76
+ }
77
+
78
+ return argv
79
+ }
80
+
46
81
  async function runCommand(command: string, argv: string[], context: CliContext): Promise<number> {
47
82
  if (command === 'help') {
48
83
  return runHelpCommand(argv)
@@ -16,7 +16,7 @@ import { formatHelpText } from '../help-text.ts'
16
16
  import { parseArgs } from '../parse-args.ts'
17
17
 
18
18
  export async function runCompletionCommand(argv: string[]): Promise<number> {
19
- if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
19
+ if (argv.length === 0) {
20
20
  process.stdout.write(getCompletionCommandHelpText())
21
21
  return 0
22
22
  }
@@ -25,6 +25,11 @@ export async function runCompletionCommand(argv: string[]): Promise<number> {
25
25
  return runCompletionPlumbing(argv.slice(1))
26
26
  }
27
27
 
28
+ if (argv.includes('-h') || argv.includes('--help')) {
29
+ process.stdout.write(getCompletionCommandHelpText())
30
+ return 0
31
+ }
32
+
28
33
  let [shell, ...rest] = argv
29
34
 
30
35
  try {
@@ -26,9 +26,9 @@ import {
26
26
  } from '../reporter.ts'
27
27
 
28
28
  const DOCTOR_SUITE_LABELS = {
29
- controllers: {
30
- complete: 'controllers',
31
- running: 'Checking controllers',
29
+ actions: {
30
+ complete: 'actions',
31
+ running: 'Checking actions',
32
32
  },
33
33
  environment: {
34
34
  complete: 'environment',
@@ -109,7 +109,7 @@ export function getDoctorCommandHelpText(target: NodeJS.WriteStream = process.st
109
109
  description: 'Exit with status 1 when warning-level findings are present',
110
110
  label: '--strict',
111
111
  },
112
- { description: 'Apply low-risk project and controller fixes', label: '--fix' },
112
+ { description: 'Apply low-risk project and action fixes', label: '--fix' },
113
113
  ],
114
114
  usage: ['remix doctor [--json] [--strict] [--fix] [--no-color]'],
115
115
  },
@@ -182,16 +182,13 @@ async function collectDoctorReport(
182
182
 
183
183
  if (hasWarningFindings(environment.suite.findings)) {
184
184
  let projectSuite = createSkippedDoctorSuite('project', 'Blocked by environment warnings.')
185
- let controllersSuite = createSkippedDoctorSuite(
186
- 'controllers',
187
- 'Blocked by environment warnings.',
188
- )
189
- suites.push(projectSuite, controllersSuite)
185
+ let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by environment warnings.')
186
+ suites.push(projectSuite, actionsSuite)
190
187
  progress?.skip(projectSuite.name, projectSuite.reason)
191
188
  writeDoctorSuiteDetails(reporter, projectSuite)
192
189
  progress?.writeSummaryGap()
193
- progress?.skip(controllersSuite.name, controllersSuite.reason)
194
- writeDoctorSuiteDetails(reporter, controllersSuite)
190
+ progress?.skip(actionsSuite.name, actionsSuite.reason)
191
+ writeDoctorSuiteDetails(reporter, actionsSuite)
195
192
  progress?.writeSummaryGap()
196
193
 
197
194
  return {
@@ -240,10 +237,10 @@ async function collectDoctorReport(
240
237
  progress?.writeSummaryGap()
241
238
 
242
239
  if (hasWarningFindings(project.suite.findings)) {
243
- let controllersSuite = createSkippedDoctorSuite('controllers', 'Blocked by project warnings.')
244
- suites.push(controllersSuite)
245
- progress?.skip(controllersSuite.name, controllersSuite.reason)
246
- writeDoctorSuiteDetails(reporter, controllersSuite)
240
+ let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by project warnings.')
241
+ suites.push(actionsSuite)
242
+ progress?.skip(actionsSuite.name, actionsSuite.reason)
243
+ writeDoctorSuiteDetails(reporter, actionsSuite)
247
244
  progress?.writeSummaryGap()
248
245
 
249
246
  return {
@@ -256,7 +253,7 @@ async function collectDoctorReport(
256
253
  }
257
254
  }
258
255
 
259
- let controllers = await runDoctorSuite(progress, 'controllers', async () => {
256
+ let actions = await runDoctorSuite(progress, 'actions', async () => {
260
257
  let controllerResult = await checkControllerConventions(
261
258
  project.routeManifest!.appRoot,
262
259
  project.routeManifest!.tree,
@@ -276,7 +273,7 @@ async function collectDoctorReport(
276
273
  remainingFindings = finalControllerResult.suite.findings
277
274
  }
278
275
 
279
- let suite = createDoctorSuite('controllers', remainingFindings)
276
+ let suite = createDoctorSuite('actions', remainingFindings)
280
277
  if (suiteAppliedFixes.length > 0) {
281
278
  suite.appliedFixes = suiteAppliedFixes
282
279
  }
@@ -284,10 +281,10 @@ async function collectDoctorReport(
284
281
  return { appliedFixes: suiteAppliedFixes, suite }
285
282
  })
286
283
 
287
- findings.push(...controllers.suite.findings)
288
- appliedFixes.push(...(controllers.appliedFixes ?? []))
289
- suites.push(controllers.suite)
290
- writeDoctorSuiteDetails(reporter, controllers.suite)
284
+ findings.push(...actions.suite.findings)
285
+ appliedFixes.push(...(actions.appliedFixes ?? []))
286
+ suites.push(actions.suite)
287
+ writeDoctorSuiteDetails(reporter, actions.suite)
291
288
  progress?.writeSummaryGap()
292
289
 
293
290
  let report: DoctorReport = {
@@ -8,7 +8,7 @@ import { createCommandReporter, type TextChannel } from '../reporter.ts'
8
8
  import { loadRouteMap, type LoadedRouteMap, type RouteTreeNode } from '../route-map.ts'
9
9
  import { lightRed } from '../terminal.ts'
10
10
 
11
- const CONTROLLERS_PATH_PREFIX = 'app/controllers/'
11
+ const ACTIONS_PATH_PREFIX = 'app/actions/'
12
12
 
13
13
  export async function runRoutesCommand(argv: string[], context: CliContext): Promise<number> {
14
14
  if (argv.includes('-h') || argv.includes('--help')) {
@@ -243,8 +243,8 @@ function colorRouteLine(line: string, node: RouteTreeNode): string {
243
243
  }
244
244
 
245
245
  function getCompactOwnerPath(ownerPath: string): string {
246
- if (ownerPath.startsWith(CONTROLLERS_PATH_PREFIX)) {
247
- return ownerPath.slice(CONTROLLERS_PATH_PREFIX.length)
246
+ if (ownerPath.startsWith(ACTIONS_PATH_PREFIX)) {
247
+ return ownerPath.slice(ACTIONS_PATH_PREFIX.length)
248
248
  }
249
249
 
250
250
  return ownerPath
@@ -169,11 +169,7 @@ function completeCommand(
169
169
  }
170
170
 
171
171
  if (command === 'test') {
172
- return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [
173
- '--coverage',
174
- '--watch',
175
- '--help',
176
- ])
172
+ return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, ['--coverage', '--watch'])
177
173
  }
178
174
 
179
175
  if (command === 'completion') {
@@ -1,22 +1,18 @@
1
1
  import * as path from 'node:path'
2
2
 
3
3
  const OWNER_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const
4
- export type OwnerFileExtension = (typeof OWNER_FILE_EXTENSIONS)[number]
4
+ const ACTIONS_DIRECTORY = path.join('app', 'actions')
5
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
- }
6
+ export type OwnerFileExtension = (typeof OWNER_FILE_EXTENSIONS)[number]
11
7
 
12
8
  export function getControllerOwnerCandidates(segments: string[]): string[] {
13
9
  return OWNER_FILE_EXTENSIONS.map((extension) =>
14
- normalizeRelativePath(path.join('app', 'controllers', ...segments, `controller${extension}`)),
10
+ normalizeRelativePath(path.join(ACTIONS_DIRECTORY, ...segments, `controller${extension}`)),
15
11
  )
16
12
  }
17
13
 
18
14
  export function getRouteSubtreePath(segments: string[]): string {
19
- return normalizeRelativePath(path.join('app', 'controllers', ...segments))
15
+ return normalizeRelativePath(path.join(ACTIONS_DIRECTORY, ...segments))
20
16
  }
21
17
 
22
18
  export function getPreferredOwnerDisplayPath(candidates: string[]): string {