@remix-run/cli 0.3.4 → 0.5.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 (119) 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 +54 -11
  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 +63 -9
  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/app/middleware/render.tsx +8 -2
  107. package/template/hmr.ts +46 -0
  108. package/template/package.json +1 -0
  109. package/template/server.ts +8 -1
  110. package/template/tsconfig.json +1 -1
  111. package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
  112. package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
  113. package/dist/lib/doctor/controller-fix-plans.js +0 -119
  114. package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
  115. package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
  116. package/dist/lib/doctor/controller-placeholders.js +0 -52
  117. package/src/lib/doctor/controller-fix-plans.ts +0 -168
  118. package/src/lib/doctor/controller-placeholders.ts +0 -70
  119. package/template/app/assets/entry.ts +0 -17
@@ -0,0 +1,377 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+ import * as process from 'node:process'
4
+
5
+ import type { Database } from '@remix-run/data-table'
6
+ import { runRemixDb } from '@remix-run/data-table/cli'
7
+ import { loadMigrations, loadSeed } from '@remix-run/data-table/migrations/node'
8
+
9
+ import { findAppRoot } from '../app-root.ts'
10
+ import type { CliContext } from '../cli-context.ts'
11
+ import {
12
+ isDatabaseCommand,
13
+ type DatabaseCommandInvocation,
14
+ type DatabaseCommandPlan,
15
+ } from '../database-command.ts'
16
+ import {
17
+ dbConfigRequired,
18
+ dbForceRequired,
19
+ invalidOptionValue,
20
+ remixConfigNotFound,
21
+ renderCliError,
22
+ toCliError,
23
+ unknownCommand,
24
+ } from '../errors.ts'
25
+ import { formatHelpText } from '../help-text.ts'
26
+ import { parseArgs } from '../parse-args.ts'
27
+ import {
28
+ loadRemixConfig,
29
+ type RemixDbAdapterConfig,
30
+ type RemixDbCommandConfig,
31
+ type RemixDbString,
32
+ } from '../remix-config.ts'
33
+
34
+ const connectionOption = { flag: '--connection-env', type: 'string' } as const
35
+ const journalOption = { flag: '--journal-table', type: 'string' } as const
36
+ const migrationsOption = { flag: '--migrations', type: 'string' } as const
37
+ const seedOption = { flag: '--seed', type: 'string' } as const
38
+
39
+ export async function runDbCommand(argv: string[], context: CliContext): Promise<number> {
40
+ if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
41
+ process.stdout.write(getDbCommandHelpText())
42
+ return 0
43
+ }
44
+
45
+ try {
46
+ let invocation = parseDbCommandArgs(argv)
47
+ let resolved = await resolveDbConfig(context)
48
+ let plan = resolveDatabaseCommandPlan(invocation, resolved.config, context.cwd)
49
+ return await runDatabaseCommand(plan, resolved.configDir)
50
+ } catch (error) {
51
+ process.stderr.write(
52
+ renderCliError(toCliError(error), { helpText: getDbCommandHelpText(process.stderr) }),
53
+ )
54
+ return 1
55
+ }
56
+ }
57
+
58
+ export function getDbCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
59
+ return formatHelpText(
60
+ {
61
+ description: 'Manage the current app database.',
62
+ examples: [
63
+ 'remix db wipe --force',
64
+ 'remix db migrate',
65
+ 'remix db migrate --to 20260715123000_add_users',
66
+ 'remix db status',
67
+ 'remix db seed',
68
+ 'remix db reset --force',
69
+ ],
70
+ options: [
71
+ {
72
+ description: 'Read the database connection from an environment variable',
73
+ label: '--connection-env <name>',
74
+ },
75
+ { description: 'Confirm a destructive command (wipe and reset only)', label: '--force' },
76
+ {
77
+ description: 'Use a migration journal table (migrate, status, and reset only)',
78
+ label: '--journal-table <name>',
79
+ },
80
+ {
81
+ description: 'Load migrations from a directory (migrate, status, and reset only)',
82
+ label: '--migrations <path>',
83
+ },
84
+ { description: 'Run a SQL seed file (seed and reset only)', label: '--seed <path>' },
85
+ {
86
+ description: 'Stop after applying the specified migration (migrate only)',
87
+ label: '--to <migration>',
88
+ },
89
+ ],
90
+ usage: [
91
+ 'remix db wipe --force [options]',
92
+ 'remix db migrate [--to <migration>] [options]',
93
+ 'remix db status [options]',
94
+ 'remix db seed [options]',
95
+ 'remix db reset --force [options]',
96
+ ],
97
+ },
98
+ target,
99
+ )
100
+ }
101
+
102
+ function parseDbCommandArgs(argv: string[]): DatabaseCommandInvocation {
103
+ let [command, ...commandArgv] = argv
104
+
105
+ if (!isDatabaseCommand(command)) {
106
+ throw unknownCommand(`db ${command}`)
107
+ }
108
+
109
+ if (command === 'migrate') {
110
+ let parsed = parseArgs(
111
+ commandArgv,
112
+ {
113
+ connectionEnv: connectionOption,
114
+ journalTable: journalOption,
115
+ migrations: migrationsOption,
116
+ to: { flag: '--to', type: 'string' },
117
+ },
118
+ { maxPositionals: 0 },
119
+ )
120
+ return { command, ...parsed.options }
121
+ }
122
+
123
+ if (command === 'reset') {
124
+ let parsed = parseArgs(
125
+ commandArgv,
126
+ {
127
+ connectionEnv: connectionOption,
128
+ force: { flag: '--force', type: 'boolean' },
129
+ journalTable: journalOption,
130
+ migrations: migrationsOption,
131
+ seed: seedOption,
132
+ },
133
+ { maxPositionals: 0 },
134
+ )
135
+ if (!parsed.options.force) throw dbForceRequired(command)
136
+ let { force: _, ...options } = parsed.options
137
+ return { command, ...options }
138
+ }
139
+
140
+ if (command === 'wipe') {
141
+ let parsed = parseArgs(
142
+ commandArgv,
143
+ {
144
+ connectionEnv: connectionOption,
145
+ force: { flag: '--force', type: 'boolean' },
146
+ },
147
+ { maxPositionals: 0 },
148
+ )
149
+ if (!parsed.options.force) throw dbForceRequired(command)
150
+ return { command, connectionEnv: parsed.options.connectionEnv }
151
+ }
152
+
153
+ if (command === 'seed') {
154
+ let parsed = parseArgs(
155
+ commandArgv,
156
+ { connectionEnv: connectionOption, seed: seedOption },
157
+ { maxPositionals: 0 },
158
+ )
159
+ return { command, ...parsed.options }
160
+ }
161
+
162
+ let parsed = parseArgs(
163
+ commandArgv,
164
+ {
165
+ connectionEnv: connectionOption,
166
+ journalTable: journalOption,
167
+ migrations: migrationsOption,
168
+ },
169
+ { maxPositionals: 0 },
170
+ )
171
+ return { command, ...parsed.options }
172
+ }
173
+
174
+ async function resolveDbConfig(
175
+ context: CliContext,
176
+ ): Promise<{ config: RemixDbCommandConfig; configDir: string }> {
177
+ let configPath: string
178
+ let config
179
+
180
+ if (context.configPath !== undefined) {
181
+ configPath = path.resolve(context.cwd, context.configPath)
182
+ config = await context.loadConfig()
183
+ } else {
184
+ let configDir = await findAppRoot(context.cwd, 'remix.json')
185
+ if (configDir === null) throw remixConfigNotFound(path.join(context.cwd, 'remix.json'))
186
+ configPath = path.join(configDir, 'remix.json')
187
+ config = await loadRemixConfig(configDir, undefined)
188
+ }
189
+
190
+ if (config.db === undefined) throw dbConfigRequired(configPath)
191
+ return { config: config.db, configDir: path.dirname(configPath) }
192
+ }
193
+
194
+ function resolveDatabaseCommandPlan(
195
+ invocation: DatabaseCommandInvocation,
196
+ config: RemixDbCommandConfig,
197
+ cwd: string,
198
+ ): DatabaseCommandPlan {
199
+ let adapter = overrideConnection(config.adapter, invocation.connectionEnv, cwd)
200
+ let migrations = invocation.migrations
201
+ ? path.resolve(cwd, invocation.migrations)
202
+ : config.migrations?.directory
203
+ let journalTable = invocation.journalTable ?? config.migrations?.journalTable
204
+ let seed = invocation.seed ? path.resolve(cwd, invocation.seed) : config.seed
205
+
206
+ if (
207
+ (invocation.command === 'migrate' ||
208
+ invocation.command === 'reset' ||
209
+ invocation.command === 'status') &&
210
+ migrations === undefined
211
+ ) {
212
+ throw invalidOptionValue(
213
+ `Database command "${invocation.command}" requires db.migrations.directory or --migrations`,
214
+ )
215
+ }
216
+
217
+ if (invocation.command === 'seed' && seed === undefined) {
218
+ throw invalidOptionValue('Database command "seed" requires db.seed or --seed')
219
+ }
220
+
221
+ return {
222
+ adapter,
223
+ command: invocation.command,
224
+ journalTable,
225
+ migrations,
226
+ seed,
227
+ to: invocation.to,
228
+ }
229
+ }
230
+
231
+ function overrideConnection(
232
+ adapter: RemixDbCommandConfig['adapter'],
233
+ environmentName: string | undefined,
234
+ cwd: string,
235
+ ): RemixDbCommandConfig['adapter'] {
236
+ if (environmentName === undefined) return adapter
237
+ if (adapter.type === 'sqlite') {
238
+ // Resolve a relative sqlite filename here, against the directory the
239
+ // command was invoked from (like --migrations and --seed). Filenames from
240
+ // remix.json resolve against the config directory instead, which would
241
+ // silently retarget the path.
242
+ let filename = process.env[environmentName]
243
+ if (filename === undefined || filename === '') {
244
+ throw invalidOptionValue(`Database environment variable ${environmentName} is not set`)
245
+ }
246
+ return {
247
+ ...adapter,
248
+ filename: filename === ':memory:' ? filename : path.resolve(cwd, filename),
249
+ }
250
+ }
251
+ if (adapter.type === 'postgres') {
252
+ return { ...adapter, connectionString: { env: environmentName } }
253
+ }
254
+ return { ...adapter, uri: { env: environmentName } }
255
+ }
256
+
257
+ async function runDatabaseCommand(plan: DatabaseCommandPlan, configDir: string): Promise<number> {
258
+ let db = await createConfiguredDatabase(plan.adapter, configDir)
259
+ let result: number
260
+
261
+ try {
262
+ result = await executeDatabaseCommand(plan, db)
263
+ } catch (error) {
264
+ try {
265
+ await db.close()
266
+ } catch (closeError) {
267
+ throw new AggregateError([error, closeError], 'Database command and close both failed', {
268
+ cause: error,
269
+ })
270
+ }
271
+ throw error
272
+ }
273
+
274
+ await db.close()
275
+ return result
276
+ }
277
+
278
+ async function executeDatabaseCommand(plan: DatabaseCommandPlan, db: Database): Promise<number> {
279
+ if (plan.command === 'wipe') {
280
+ return runRemixDb({ command: plan.command, db })
281
+ }
282
+
283
+ if (plan.command === 'seed') {
284
+ if (plan.seed === undefined) {
285
+ throw invalidOptionValue('Database command "seed" requires db.seed or --seed')
286
+ }
287
+ return runRemixDb({ command: plan.command, db, seed: await loadSeed(plan.seed) })
288
+ }
289
+
290
+ if (plan.migrations === undefined) {
291
+ throw invalidOptionValue(
292
+ `Database command "${plan.command}" requires db.migrations.directory or --migrations`,
293
+ )
294
+ }
295
+
296
+ let migrations = await loadMigrations(plan.migrations)
297
+
298
+ if (plan.command === 'migrate') {
299
+ return runRemixDb({
300
+ command: plan.command,
301
+ db,
302
+ migrations,
303
+ to: plan.to,
304
+ journalTable: plan.journalTable,
305
+ })
306
+ }
307
+
308
+ if (plan.command === 'reset') {
309
+ let seed = plan.seed === undefined ? undefined : await loadSeed(plan.seed)
310
+ return runRemixDb({
311
+ command: plan.command,
312
+ db,
313
+ migrations,
314
+ seed,
315
+ journalTable: plan.journalTable,
316
+ })
317
+ }
318
+
319
+ return runRemixDb({
320
+ command: plan.command,
321
+ db,
322
+ migrations,
323
+ journalTable: plan.journalTable,
324
+ })
325
+ }
326
+
327
+ async function createConfiguredDatabase(
328
+ adapter: RemixDbAdapterConfig,
329
+ configDir: string,
330
+ ): Promise<Database> {
331
+ // Adapter packages are imported lazily because their database drivers are
332
+ // optional peer dependencies; only the configured adapter's driver needs to
333
+ // be installed.
334
+ if (adapter.type === 'sqlite') {
335
+ let { createSqliteDatabase } = await import('@remix-run/data-table-sqlite')
336
+ let filename = resolveDbString(adapter.filename)
337
+ if (filename !== ':memory:') {
338
+ // Filenames from remix.json are config-relative; explicit flag overrides
339
+ // arrive here already resolved against the invocation directory.
340
+ filename = path.resolve(configDir, filename)
341
+ await fs.mkdir(path.dirname(filename), { recursive: true })
342
+ }
343
+ return createSqliteDatabase({
344
+ filename,
345
+ foreignKeys: adapter.foreignKeys,
346
+ busyTimeout: adapter.busyTimeout,
347
+ })
348
+ }
349
+
350
+ if (adapter.type === 'postgres') {
351
+ let { createPostgresDatabase } = await import('@remix-run/data-table-postgres')
352
+ return createPostgresDatabase(
353
+ { connectionString: resolveDbString(adapter.connectionString) },
354
+ {
355
+ maintenanceDatabase: adapter.maintenanceDatabase,
356
+ template: adapter.template,
357
+ },
358
+ )
359
+ }
360
+
361
+ let { createMysqlDatabase } = await import('@remix-run/data-table-mysql')
362
+ return createMysqlDatabase(
363
+ { uri: resolveDbString(adapter.uri), multipleStatements: true },
364
+ { characterSet: adapter.characterSet, collation: adapter.collation },
365
+ )
366
+ }
367
+
368
+ function resolveDbString(value: RemixDbString): string {
369
+ if (typeof value === 'string') return value
370
+ // An env var that is set but empty is treated as unset so it falls back to
371
+ // the configured default instead of producing an empty connection value.
372
+ let resolved = process.env[value.env] || value.default
373
+ if (resolved === undefined || resolved === '') {
374
+ throw invalidOptionValue(`Database environment variable ${value.env} is not set`)
375
+ }
376
+ return resolved
377
+ }