@remix-run/cli 0.3.4 → 0.4.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 (118) hide show
  1. package/README.md +105 -1
  2. package/dist/index.js +1 -1
  3. package/dist/lib/app-root.d.ts +11 -0
  4. package/dist/lib/app-root.d.ts.map +1 -0
  5. package/dist/lib/app-root.js +38 -0
  6. package/dist/lib/bootstrap-project.js +2 -2
  7. package/dist/lib/cli-context.d.ts +6 -0
  8. package/dist/lib/cli-context.d.ts.map +1 -1
  9. package/dist/lib/cli-context.js +12 -1
  10. package/dist/lib/cli.d.ts +3 -2
  11. package/dist/lib/cli.d.ts.map +1 -1
  12. package/dist/lib/cli.js +42 -14
  13. package/dist/lib/commands/completion.js +4 -4
  14. package/dist/lib/commands/db.d.ts +4 -0
  15. package/dist/lib/commands/db.d.ts.map +1 -0
  16. package/dist/lib/commands/db.js +283 -0
  17. package/dist/lib/commands/doctor.d.ts +5 -0
  18. package/dist/lib/commands/doctor.d.ts.map +1 -1
  19. package/dist/lib/commands/doctor.js +26 -251
  20. package/dist/lib/commands/help.d.ts.map +1 -1
  21. package/dist/lib/commands/help.js +17 -9
  22. package/dist/lib/commands/new.js +6 -6
  23. package/dist/lib/commands/routes.js +6 -6
  24. package/dist/lib/commands/test.d.ts +20 -1
  25. package/dist/lib/commands/test.d.ts.map +1 -1
  26. package/dist/lib/commands/test.js +353 -10
  27. package/dist/lib/commands/version.js +3 -3
  28. package/dist/lib/completion.d.ts +1 -1
  29. package/dist/lib/completion.d.ts.map +1 -1
  30. package/dist/lib/completion.js +229 -6
  31. package/dist/lib/controller-files.d.ts +0 -8
  32. package/dist/lib/controller-files.d.ts.map +1 -1
  33. package/dist/lib/controller-files.js +0 -23
  34. package/dist/lib/controller-ownership.d.ts +4 -12
  35. package/dist/lib/controller-ownership.d.ts.map +1 -1
  36. package/dist/lib/controller-ownership.js +31 -127
  37. package/dist/lib/database-command.d.ts +20 -0
  38. package/dist/lib/database-command.d.ts.map +1 -0
  39. package/dist/lib/database-command.js +7 -0
  40. package/dist/lib/doctor/controller-findings.d.ts +2 -2
  41. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  42. package/dist/lib/doctor/controller-findings.js +23 -39
  43. package/dist/lib/doctor/controllers.d.ts +1 -2
  44. package/dist/lib/doctor/controllers.d.ts.map +1 -1
  45. package/dist/lib/doctor/controllers.js +4 -7
  46. package/dist/lib/doctor/environment.js +2 -2
  47. package/dist/lib/doctor/fixes.js +1 -1
  48. package/dist/lib/doctor/project.d.ts.map +1 -1
  49. package/dist/lib/doctor/project.js +3 -212
  50. package/dist/lib/doctor/run.d.ts +9 -0
  51. package/dist/lib/doctor/run.d.ts.map +1 -0
  52. package/dist/lib/doctor/run.js +237 -0
  53. package/dist/lib/doctor/types.d.ts +2 -1
  54. package/dist/lib/doctor/types.d.ts.map +1 -1
  55. package/dist/lib/doctor/types.js +4 -1
  56. package/dist/lib/errors.d.ts +30 -0
  57. package/dist/lib/errors.d.ts.map +1 -1
  58. package/dist/lib/errors.js +55 -0
  59. package/dist/lib/help-text.js +1 -1
  60. package/dist/lib/parse-args.d.ts.map +1 -1
  61. package/dist/lib/parse-args.js +17 -4
  62. package/dist/lib/remix-config.d.ts +75 -0
  63. package/dist/lib/remix-config.d.ts.map +1 -0
  64. package/dist/lib/remix-config.js +411 -0
  65. package/dist/lib/reporter.js +1 -1
  66. package/dist/lib/route-map.js +13 -33
  67. package/package.json +13 -7
  68. package/schema/remix.json +242 -0
  69. package/src/lib/app-root.ts +46 -0
  70. package/src/lib/cli-context.ts +17 -0
  71. package/src/lib/cli.ts +41 -6
  72. package/src/lib/commands/db.ts +377 -0
  73. package/src/lib/commands/doctor.ts +27 -329
  74. package/src/lib/commands/help.ts +10 -1
  75. package/src/lib/commands/test.ts +453 -12
  76. package/src/lib/completion.ts +321 -6
  77. package/src/lib/controller-files.ts +0 -38
  78. package/src/lib/controller-ownership.ts +50 -203
  79. package/src/lib/database-command.ts +31 -0
  80. package/src/lib/doctor/controller-findings.ts +30 -47
  81. package/src/lib/doctor/controllers.ts +2 -6
  82. package/src/lib/doctor/project.ts +0 -236
  83. package/src/lib/doctor/run.ts +311 -0
  84. package/src/lib/doctor/types.ts +6 -5
  85. package/src/lib/errors.ts +65 -0
  86. package/src/lib/parse-args.ts +19 -3
  87. package/src/lib/remix-config.ts +639 -0
  88. package/src/lib/route-map.ts +11 -48
  89. package/template/.agents/skills/remix/SKILL.md +38 -27
  90. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +53 -10
  91. package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
  92. package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
  93. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +54 -8
  94. package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
  95. package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
  96. package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
  97. package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
  98. package/template/AGENTS.md +6 -3
  99. package/template/README.md +6 -3
  100. package/template/app/actions/controller.tsx +1 -1
  101. package/template/app/{ui → actions}/document.tsx +7 -5
  102. package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
  103. package/template/app/actions/public/entry.ts +48 -0
  104. package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
  105. package/template/app/assets.ts +15 -2
  106. package/template/hmr.ts +46 -0
  107. package/template/package.json +1 -0
  108. package/template/server.ts +8 -1
  109. package/template/tsconfig.json +1 -1
  110. package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
  111. package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
  112. package/dist/lib/doctor/controller-fix-plans.js +0 -119
  113. package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
  114. package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
  115. package/dist/lib/doctor/controller-placeholders.js +0 -52
  116. package/src/lib/doctor/controller-fix-plans.ts +0 -168
  117. package/src/lib/doctor/controller-placeholders.ts +0 -70
  118. package/template/app/assets/entry.ts +0 -17
@@ -0,0 +1,242 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://remix.run/schemas/remix.json",
4
+ "title": "Remix CLI configuration",
5
+ "description": "Configuration for the Remix command-line interface.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "description": "JSON Schema used by editors to validate this file."
12
+ },
13
+ "db": {
14
+ "$ref": "#/$defs/db"
15
+ },
16
+ "doctor": {
17
+ "$ref": "#/$defs/doctor"
18
+ },
19
+ "test": {
20
+ "$ref": "#/$defs/test"
21
+ }
22
+ },
23
+ "$defs": {
24
+ "db": {
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "required": ["adapter"],
28
+ "properties": {
29
+ "adapter": {
30
+ "oneOf": [
31
+ { "$ref": "#/$defs/dbSqliteAdapter" },
32
+ { "$ref": "#/$defs/dbPostgresAdapter" },
33
+ { "$ref": "#/$defs/dbMysqlAdapter" }
34
+ ]
35
+ },
36
+ "migrations": {
37
+ "$ref": "#/$defs/dbMigrations"
38
+ },
39
+ "seed": {
40
+ "type": "string",
41
+ "description": "Path to a SQL seed file."
42
+ }
43
+ }
44
+ },
45
+ "dbString": {
46
+ "oneOf": [
47
+ { "type": "string" },
48
+ {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "required": ["env"],
52
+ "properties": {
53
+ "env": { "type": "string" },
54
+ "default": { "type": "string" }
55
+ }
56
+ }
57
+ ]
58
+ },
59
+ "dbSqliteAdapter": {
60
+ "type": "object",
61
+ "additionalProperties": false,
62
+ "required": ["type", "filename"],
63
+ "properties": {
64
+ "type": { "const": "sqlite" },
65
+ "filename": { "$ref": "#/$defs/dbString" },
66
+ "foreignKeys": { "type": "boolean" },
67
+ "busyTimeout": { "type": "number", "minimum": 0 }
68
+ }
69
+ },
70
+ "dbPostgresAdapter": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["type", "connectionString"],
74
+ "properties": {
75
+ "type": { "const": "postgres" },
76
+ "connectionString": { "$ref": "#/$defs/dbString" },
77
+ "maintenanceDatabase": { "type": "string" },
78
+ "template": { "type": "string" }
79
+ }
80
+ },
81
+ "dbMysqlAdapter": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "required": ["type", "uri"],
85
+ "properties": {
86
+ "type": { "const": "mysql" },
87
+ "uri": { "$ref": "#/$defs/dbString" },
88
+ "characterSet": { "type": "string" },
89
+ "collation": { "type": "string" }
90
+ }
91
+ },
92
+ "dbMigrations": {
93
+ "type": "object",
94
+ "additionalProperties": false,
95
+ "required": ["directory"],
96
+ "properties": {
97
+ "directory": { "type": "string" },
98
+ "journalTable": { "type": "string" }
99
+ }
100
+ },
101
+ "doctor": {
102
+ "type": "object",
103
+ "additionalProperties": false,
104
+ "properties": {
105
+ "strict": {
106
+ "type": "boolean",
107
+ "description": "Whether doctor exits with status 1 when warning-level findings are present."
108
+ }
109
+ }
110
+ },
111
+ "stringArray": {
112
+ "type": "array",
113
+ "items": {
114
+ "type": "string"
115
+ }
116
+ },
117
+ "test": {
118
+ "type": "object",
119
+ "additionalProperties": false,
120
+ "properties": {
121
+ "browserFiles": {
122
+ "$ref": "#/$defs/stringArray",
123
+ "description": "Glob patterns for browser test files."
124
+ },
125
+ "concurrency": {
126
+ "type": "integer",
127
+ "minimum": 1,
128
+ "description": "Maximum number of concurrent test workers."
129
+ },
130
+ "coverage": {
131
+ "$ref": "#/$defs/coverage"
132
+ },
133
+ "e2eFiles": {
134
+ "$ref": "#/$defs/stringArray",
135
+ "description": "Glob patterns for end-to-end test files."
136
+ },
137
+ "exclude": {
138
+ "$ref": "#/$defs/stringArray",
139
+ "description": "Glob patterns excluded from test discovery."
140
+ },
141
+ "files": {
142
+ "$ref": "#/$defs/stringArray",
143
+ "description": "Glob patterns for all test files."
144
+ },
145
+ "only": {
146
+ "$ref": "#/$defs/stringArray",
147
+ "description": "Regular expression patterns used to focus tests by full name."
148
+ },
149
+ "playwright": {
150
+ "$ref": "#/$defs/playwright"
151
+ },
152
+ "pool": {
153
+ "enum": ["forks", "threads"],
154
+ "description": "Worker pool used for server and end-to-end tests."
155
+ },
156
+ "quiet": {
157
+ "type": "boolean",
158
+ "description": "Whether skipped tests are omitted from reporter output."
159
+ },
160
+ "reporter": {
161
+ "enum": ["spec", "files", "tap", "dot"],
162
+ "description": "Reporter used for test output."
163
+ },
164
+ "setup": {
165
+ "type": "string",
166
+ "description": "Path to a module exporting globalSetup and/or globalTeardown."
167
+ },
168
+ "type": {
169
+ "type": "array",
170
+ "items": {
171
+ "enum": ["server", "browser", "e2e"]
172
+ },
173
+ "description": "Test types to run."
174
+ },
175
+ "watch": {
176
+ "type": "boolean",
177
+ "description": "Whether tests rerun when files change."
178
+ }
179
+ }
180
+ },
181
+ "coverage": {
182
+ "type": "object",
183
+ "additionalProperties": false,
184
+ "properties": {
185
+ "branches": {
186
+ "type": "number",
187
+ "description": "Minimum branch coverage percentage."
188
+ },
189
+ "dir": {
190
+ "type": "string",
191
+ "description": "Directory where coverage reports are written."
192
+ },
193
+ "enabled": {
194
+ "type": "boolean",
195
+ "description": "Whether coverage is collected."
196
+ },
197
+ "exclude": {
198
+ "$ref": "#/$defs/stringArray",
199
+ "description": "Glob patterns excluded from coverage."
200
+ },
201
+ "functions": {
202
+ "type": "number",
203
+ "description": "Minimum function coverage percentage."
204
+ },
205
+ "include": {
206
+ "$ref": "#/$defs/stringArray",
207
+ "description": "Glob patterns included in coverage."
208
+ },
209
+ "lines": {
210
+ "type": "number",
211
+ "description": "Minimum line coverage percentage."
212
+ },
213
+ "statements": {
214
+ "type": "number",
215
+ "description": "Minimum statement coverage percentage."
216
+ }
217
+ }
218
+ },
219
+ "playwright": {
220
+ "type": "object",
221
+ "additionalProperties": false,
222
+ "properties": {
223
+ "configFile": {
224
+ "type": "string",
225
+ "description": "Path to a Playwright configuration file."
226
+ },
227
+ "echo": {
228
+ "type": "boolean",
229
+ "description": "Whether browser console output is echoed to stdout."
230
+ },
231
+ "open": {
232
+ "type": "boolean",
233
+ "description": "Whether the browser remains open after tests finish."
234
+ },
235
+ "projects": {
236
+ "$ref": "#/$defs/stringArray",
237
+ "description": "Playwright project names to run."
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
@@ -0,0 +1,46 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+
4
+ /**
5
+ * Walks up from `startDir` looking for the closest directory that contains
6
+ * `relativeFilePath`.
7
+ *
8
+ * @param startDir Directory the walk starts from.
9
+ * @param relativeFilePath Slash-separated app file path, for example `app/routes.ts`.
10
+ * @returns The closest ancestor directory that contains the file, or `null`
11
+ * when no ancestor contains it.
12
+ */
13
+ export async function findAppRoot(
14
+ startDir: string,
15
+ relativeFilePath: string,
16
+ ): Promise<string | null> {
17
+ let relativeSegments = relativeFilePath.split('/')
18
+ let currentDir = path.resolve(startDir)
19
+
20
+ while (true) {
21
+ if (await pathExists(path.join(currentDir, ...relativeSegments))) {
22
+ return currentDir
23
+ }
24
+
25
+ let parentDir = path.dirname(currentDir)
26
+ if (parentDir === currentDir) {
27
+ return null
28
+ }
29
+
30
+ currentDir = parentDir
31
+ }
32
+ }
33
+
34
+ async function pathExists(filePath: string): Promise<boolean> {
35
+ try {
36
+ await fs.access(filePath)
37
+ return true
38
+ } catch (error) {
39
+ let nodeError = error as NodeJS.ErrnoException
40
+ if (nodeError.code === 'ENOENT') {
41
+ return false
42
+ }
43
+
44
+ throw error
45
+ }
46
+ }
@@ -2,14 +2,20 @@ import * as path from 'node:path'
2
2
  import * as process from 'node:process'
3
3
 
4
4
  import { resolveDefaultRemixVersion } from './remix-version.ts'
5
+ import { loadRemixConfig, type RemixConfig } from './remix-config.ts'
5
6
 
6
7
  interface ResolveCliContextOptions {
8
+ configPath?: string
7
9
  cwd?: string
8
10
  remixVersion?: string
9
11
  }
10
12
 
11
13
  export interface CliContext {
14
+ /** Explicit Remix configuration path selected by the caller. */
15
+ configPath?: string
12
16
  cwd: string
17
+ /** Loads and validates the Remix config file, memoizing the result. */
18
+ loadConfig(): Promise<RemixConfig>
13
19
  remixVersion?: string
14
20
  }
15
21
 
@@ -19,8 +25,19 @@ export async function resolveCliContext(
19
25
  let cwd = resolveCwd(options.cwd)
20
26
  let remixVersion = normalizeRemixVersion(options.remixVersion)
21
27
 
28
+ let configPromise: Promise<RemixConfig> | undefined
29
+ let loadConfig = () => (configPromise ??= loadRemixConfig(cwd, options.configPath))
30
+
31
+ // Validate an explicitly selected config file up front so every command
32
+ // fails fast on a bad --config path. The default remix.json is loaded
33
+ // lazily by the commands that read it, so an invalid file doesn't break
34
+ // unrelated commands like help, version, or shell completion.
35
+ if (options.configPath !== undefined) await loadConfig()
36
+
22
37
  return {
38
+ configPath: options.configPath,
23
39
  cwd,
40
+ loadConfig,
24
41
  remixVersion: remixVersion ?? (await resolveDefaultRemixVersion(cwd)),
25
42
  }
26
43
  }
package/src/lib/cli.ts CHANGED
@@ -2,7 +2,7 @@ import * as process from 'node:process'
2
2
 
3
3
  import { getCliHelpText, runHelpCommand } from './commands/help.ts'
4
4
  import { resolveCliContext, type CliContext } from './cli-context.ts'
5
- import { renderCliError, unknownCommand } from './errors.ts'
5
+ import { missingOptionValue, renderCliError, toCliError, unknownCommand } from './errors.ts'
6
6
  import { configureColors, restoreTerminalFormatting } from './terminal.ts'
7
7
 
8
8
  /**
@@ -23,8 +23,9 @@ export interface RunRemixOptions {
23
23
 
24
24
  /**
25
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.
26
+ * subcommand (`new`, `db`, `doctor`, `routes`, `test`, `version`,
27
+ * `completion`, `help`), and resolves with the exit code the process should
28
+ * use.
28
29
  *
29
30
  * @param argv Argument vector to parse, excluding the node and script paths
30
31
  * (default `process.argv.slice(2)`).
@@ -43,14 +44,13 @@ export async function runRemix(
43
44
  argv: string[] = process.argv.slice(2),
44
45
  options: RunRemixOptions = {},
45
46
  ): Promise<number> {
46
- let context = await resolveCliContext(options)
47
-
48
47
  try {
49
48
  argv = stripLeadingSeparators(argv)
50
49
 
51
50
  let globalOptions = extractGlobalOptions(argv)
52
51
  argv = stripLeadingSeparators(globalOptions.argv)
53
52
  configureColors({ disabled: globalOptions.noColor })
53
+ let context = await resolveCliContext({ ...options, configPath: globalOptions.configPath })
54
54
 
55
55
  if (argv.length === 0) {
56
56
  process.stdout.write(getCliHelpText())
@@ -65,6 +65,11 @@ export async function runRemix(
65
65
  }
66
66
 
67
67
  return await runCommand(command, rest, context)
68
+ } catch (error) {
69
+ process.stderr.write(
70
+ renderCliError(toCliError(error), { helpText: getCliHelpText(process.stderr) }),
71
+ )
72
+ return 1
68
73
  } finally {
69
74
  restoreTerminalFormatting()
70
75
  }
@@ -103,6 +108,11 @@ async function runCommand(command: string, argv: string[], context: CliContext):
103
108
  return runDoctorCommand(argv, context)
104
109
  }
105
110
 
111
+ if (command === 'db') {
112
+ let { runDbCommand } = await import('./commands/db.ts')
113
+ return runDbCommand(argv, context)
114
+ }
115
+
106
116
  if (command === 'routes') {
107
117
  let { runRoutesCommand } = await import('./commands/routes.ts')
108
118
  return runRoutesCommand(argv, context)
@@ -124,8 +134,13 @@ async function runCommand(command: string, argv: string[], context: CliContext):
124
134
  return 1
125
135
  }
126
136
 
127
- function extractGlobalOptions(argv: string[]): { argv: string[]; noColor: boolean } {
137
+ function extractGlobalOptions(argv: string[]): {
138
+ argv: string[]
139
+ configPath?: string
140
+ noColor: boolean
141
+ } {
128
142
  let filteredArgv: string[] = []
143
+ let configPath: string | undefined
129
144
  let noColor = false
130
145
 
131
146
  for (let index = 0; index < argv.length; index++) {
@@ -141,11 +156,31 @@ function extractGlobalOptions(argv: string[]): { argv: string[]; noColor: boolea
141
156
  continue
142
157
  }
143
158
 
159
+ if (arg === '--config') {
160
+ let value = argv[index + 1]
161
+ if (value == null || value.length === 0 || value.startsWith('-')) {
162
+ throw missingOptionValue('--config')
163
+ }
164
+ configPath = value
165
+ index++
166
+ continue
167
+ }
168
+
169
+ if (arg.startsWith('--config=')) {
170
+ let value = arg.slice('--config='.length)
171
+ if (value.length === 0) {
172
+ throw missingOptionValue('--config')
173
+ }
174
+ configPath = value
175
+ continue
176
+ }
177
+
144
178
  filteredArgv.push(arg)
145
179
  }
146
180
 
147
181
  return {
148
182
  argv: filteredArgv,
183
+ configPath,
149
184
  noColor,
150
185
  }
151
186
  }