@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.
- package/README.md +105 -1
- package/dist/index.js +1 -1
- package/dist/lib/app-root.d.ts +11 -0
- package/dist/lib/app-root.d.ts.map +1 -0
- package/dist/lib/app-root.js +38 -0
- package/dist/lib/bootstrap-project.js +2 -2
- package/dist/lib/cli-context.d.ts +6 -0
- package/dist/lib/cli-context.d.ts.map +1 -1
- package/dist/lib/cli-context.js +12 -1
- package/dist/lib/cli.d.ts +3 -2
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +42 -14
- package/dist/lib/commands/completion.js +4 -4
- package/dist/lib/commands/db.d.ts +4 -0
- package/dist/lib/commands/db.d.ts.map +1 -0
- package/dist/lib/commands/db.js +283 -0
- package/dist/lib/commands/doctor.d.ts +5 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -1
- package/dist/lib/commands/doctor.js +26 -251
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +17 -9
- package/dist/lib/commands/new.js +6 -6
- package/dist/lib/commands/routes.js +6 -6
- package/dist/lib/commands/test.d.ts +20 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +353 -10
- package/dist/lib/commands/version.js +3 -3
- package/dist/lib/completion.d.ts +1 -1
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +229 -6
- package/dist/lib/controller-files.d.ts +0 -8
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +0 -23
- package/dist/lib/controller-ownership.d.ts +4 -12
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +31 -127
- package/dist/lib/database-command.d.ts +20 -0
- package/dist/lib/database-command.d.ts.map +1 -0
- package/dist/lib/database-command.js +7 -0
- package/dist/lib/doctor/controller-findings.d.ts +2 -2
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +23 -39
- package/dist/lib/doctor/controllers.d.ts +1 -2
- package/dist/lib/doctor/controllers.d.ts.map +1 -1
- package/dist/lib/doctor/controllers.js +4 -7
- package/dist/lib/doctor/environment.js +2 -2
- package/dist/lib/doctor/fixes.js +1 -1
- package/dist/lib/doctor/project.d.ts.map +1 -1
- package/dist/lib/doctor/project.js +3 -212
- package/dist/lib/doctor/run.d.ts +9 -0
- package/dist/lib/doctor/run.d.ts.map +1 -0
- package/dist/lib/doctor/run.js +237 -0
- package/dist/lib/doctor/types.d.ts +2 -1
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/doctor/types.js +4 -1
- package/dist/lib/errors.d.ts +30 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +55 -0
- package/dist/lib/help-text.js +1 -1
- package/dist/lib/parse-args.d.ts.map +1 -1
- package/dist/lib/parse-args.js +17 -4
- package/dist/lib/remix-config.d.ts +75 -0
- package/dist/lib/remix-config.d.ts.map +1 -0
- package/dist/lib/remix-config.js +411 -0
- package/dist/lib/reporter.js +1 -1
- package/dist/lib/route-map.js +13 -33
- package/package.json +13 -7
- package/schema/remix.json +242 -0
- package/src/lib/app-root.ts +46 -0
- package/src/lib/cli-context.ts +17 -0
- package/src/lib/cli.ts +41 -6
- package/src/lib/commands/db.ts +377 -0
- package/src/lib/commands/doctor.ts +27 -329
- package/src/lib/commands/help.ts +10 -1
- package/src/lib/commands/test.ts +453 -12
- package/src/lib/completion.ts +321 -6
- package/src/lib/controller-files.ts +0 -38
- package/src/lib/controller-ownership.ts +50 -203
- package/src/lib/database-command.ts +31 -0
- package/src/lib/doctor/controller-findings.ts +30 -47
- package/src/lib/doctor/controllers.ts +2 -6
- package/src/lib/doctor/project.ts +0 -236
- package/src/lib/doctor/run.ts +311 -0
- package/src/lib/doctor/types.ts +6 -5
- package/src/lib/errors.ts +65 -0
- package/src/lib/parse-args.ts +19 -3
- package/src/lib/remix-config.ts +639 -0
- package/src/lib/route-map.ts +11 -48
- package/template/.agents/skills/remix/SKILL.md +38 -27
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +53 -10
- package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
- package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +54 -8
- package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
- package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
- package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
- package/template/AGENTS.md +6 -3
- package/template/README.md +6 -3
- package/template/app/actions/controller.tsx +1 -1
- package/template/app/{ui → actions}/document.tsx +7 -5
- package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
- package/template/app/actions/public/entry.ts +48 -0
- package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
- package/template/app/assets.ts +15 -2
- package/template/hmr.ts +46 -0
- package/template/package.json +1 -0
- package/template/server.ts +8 -1
- package/template/tsconfig.json +1 -1
- package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
- package/dist/lib/doctor/controller-fix-plans.js +0 -119
- package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
- package/dist/lib/doctor/controller-placeholders.js +0 -52
- package/src/lib/doctor/controller-fix-plans.ts +0 -168
- package/src/lib/doctor/controller-placeholders.ts +0 -70
- package/template/app/assets/entry.ts +0 -17
package/src/lib/commands/test.ts
CHANGED
|
@@ -1,31 +1,472 @@
|
|
|
1
1
|
import * as process from 'node:process'
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
import type { RemixTestConfig, RemixTestPool } from '@remix-run/test/cli'
|
|
2
4
|
|
|
3
5
|
import type { CliContext } from '../cli-context.ts'
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
invalidOptionValue,
|
|
8
|
+
missingOptionValue,
|
|
9
|
+
renderCliError,
|
|
10
|
+
toCliError,
|
|
11
|
+
unknownArgument,
|
|
12
|
+
} from '../errors.ts'
|
|
13
|
+
import { formatHelpText } from '../help-text.ts'
|
|
14
|
+
import type { RemixTestCommandConfig } from '../remix-config.ts'
|
|
15
|
+
|
|
16
|
+
const testCommandOptions = {
|
|
17
|
+
'browser.echo': { type: 'boolean' },
|
|
18
|
+
'no-browser.echo': { type: 'boolean' },
|
|
19
|
+
'browser.open': { type: 'boolean' },
|
|
20
|
+
'no-browser.open': { type: 'boolean' },
|
|
21
|
+
'glob.browser': { type: 'string', multiple: true },
|
|
22
|
+
'glob.e2e': { type: 'string', multiple: true },
|
|
23
|
+
'glob.exclude': { type: 'string', multiple: true },
|
|
24
|
+
'glob.test': { type: 'string', multiple: true },
|
|
25
|
+
concurrency: { type: 'string', short: 'c' },
|
|
26
|
+
coverage: { type: 'boolean' },
|
|
27
|
+
'no-coverage': { type: 'boolean' },
|
|
28
|
+
'coverage.dir': { type: 'string' },
|
|
29
|
+
'coverage.include': { type: 'string', multiple: true },
|
|
30
|
+
'coverage.exclude': { type: 'string', multiple: true },
|
|
31
|
+
'coverage.branches': { type: 'string' },
|
|
32
|
+
'coverage.functions': { type: 'string' },
|
|
33
|
+
'coverage.lines': { type: 'string' },
|
|
34
|
+
'coverage.statements': { type: 'string' },
|
|
35
|
+
setup: { type: 'string', short: 's' },
|
|
36
|
+
playwrightConfig: { type: 'string' },
|
|
37
|
+
project: { type: 'string', short: 'p', multiple: true },
|
|
38
|
+
pool: { type: 'string' },
|
|
39
|
+
quiet: { type: 'boolean', short: 'q' },
|
|
40
|
+
'no-quiet': { type: 'boolean' },
|
|
41
|
+
only: { type: 'string', multiple: true },
|
|
42
|
+
reporter: { type: 'string', short: 'r' },
|
|
43
|
+
type: { type: 'string', short: 't', multiple: true },
|
|
44
|
+
watch: { type: 'boolean', short: 'w' },
|
|
45
|
+
'no-watch': { type: 'boolean' },
|
|
46
|
+
} as const
|
|
47
|
+
|
|
48
|
+
interface TestFlagMeta {
|
|
49
|
+
/** Base help description; "(repeatable)" and "(default: ...)" are appended automatically. */
|
|
50
|
+
description: string
|
|
51
|
+
/** Placeholder for the flag's value in help text, e.g. `glob` renders as `<glob>`. */
|
|
52
|
+
valueHint?: string
|
|
53
|
+
/** Complete this flag's value as a file path in shell completion. */
|
|
54
|
+
files?: boolean
|
|
55
|
+
/** Default value shown in help text. */
|
|
56
|
+
default?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The annotation keeps this table in lockstep with `testCommandOptions`: adding
|
|
60
|
+
// or removing a flag without updating its metadata is a compile error.
|
|
61
|
+
const testCommandFlagMeta: Record<keyof typeof testCommandOptions, TestFlagMeta> = {
|
|
62
|
+
'browser.echo': { description: 'Echo browser console output' },
|
|
63
|
+
'no-browser.echo': { description: 'Do not echo browser console output' },
|
|
64
|
+
'browser.open': { description: 'Open the browser after tests finish' },
|
|
65
|
+
'no-browser.open': { description: 'Do not open the browser after tests finish' },
|
|
66
|
+
'glob.browser': {
|
|
67
|
+
description: 'Glob pattern for browser test files',
|
|
68
|
+
valueHint: 'glob',
|
|
69
|
+
files: true,
|
|
70
|
+
},
|
|
71
|
+
'glob.e2e': { description: 'Glob pattern for E2E test files', valueHint: 'glob', files: true },
|
|
72
|
+
'glob.exclude': {
|
|
73
|
+
description: 'Glob pattern to exclude from discovery',
|
|
74
|
+
valueHint: 'glob',
|
|
75
|
+
files: true,
|
|
76
|
+
},
|
|
77
|
+
'glob.test': {
|
|
78
|
+
description: 'Glob pattern for all test files',
|
|
79
|
+
valueHint: 'glob',
|
|
80
|
+
files: true,
|
|
81
|
+
default: '**/*.test{,.e2e,.browser}.{ts,tsx}',
|
|
82
|
+
},
|
|
83
|
+
concurrency: {
|
|
84
|
+
description: 'Maximum concurrent test workers',
|
|
85
|
+
valueHint: 'count',
|
|
86
|
+
default: 'os.availableParallelism()',
|
|
87
|
+
},
|
|
88
|
+
coverage: { description: 'Collect test coverage', default: 'false' },
|
|
89
|
+
'no-coverage': { description: 'Do not collect test coverage' },
|
|
90
|
+
'coverage.dir': {
|
|
91
|
+
description: 'Coverage report output directory',
|
|
92
|
+
valueHint: 'path',
|
|
93
|
+
default: '.coverage',
|
|
94
|
+
},
|
|
95
|
+
'coverage.include': { description: 'Coverage inclusion glob', valueHint: 'glob' },
|
|
96
|
+
'coverage.exclude': { description: 'Coverage exclusion glob', valueHint: 'glob' },
|
|
97
|
+
'coverage.branches': { description: 'Minimum branch coverage percentage', valueHint: 'percent' },
|
|
98
|
+
'coverage.functions': {
|
|
99
|
+
description: 'Minimum function coverage percentage',
|
|
100
|
+
valueHint: 'percent',
|
|
101
|
+
},
|
|
102
|
+
'coverage.lines': { description: 'Minimum line coverage percentage', valueHint: 'percent' },
|
|
103
|
+
'coverage.statements': {
|
|
104
|
+
description: 'Minimum statement coverage percentage',
|
|
105
|
+
valueHint: 'percent',
|
|
106
|
+
},
|
|
107
|
+
setup: { description: 'Path to a setup module', valueHint: 'path', files: true },
|
|
108
|
+
playwrightConfig: {
|
|
109
|
+
description: 'Path to a Playwright config file',
|
|
110
|
+
valueHint: 'path',
|
|
111
|
+
files: true,
|
|
112
|
+
},
|
|
113
|
+
project: { description: 'Playwright project name', valueHint: 'name' },
|
|
114
|
+
pool: { description: 'Worker pool: forks or threads', valueHint: 'pool', default: 'forks' },
|
|
115
|
+
quiet: { description: 'Do not print skipped tests' },
|
|
116
|
+
'no-quiet': { description: 'Print skipped tests' },
|
|
117
|
+
only: { description: 'Test name pattern', valueHint: 'pattern' },
|
|
118
|
+
reporter: {
|
|
119
|
+
description: 'Reporter: spec, files, tap, or dot',
|
|
120
|
+
valueHint: 'name',
|
|
121
|
+
default: 'spec',
|
|
122
|
+
},
|
|
123
|
+
type: {
|
|
124
|
+
description: 'Test type: server, browser, or e2e',
|
|
125
|
+
valueHint: 'type',
|
|
126
|
+
default: 'server, browser, e2e',
|
|
127
|
+
},
|
|
128
|
+
watch: { description: 'Re-run tests when files change' },
|
|
129
|
+
'no-watch': { description: 'Do not re-run tests when files change' },
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Flag descriptors for `remix test`, derived from the parse table so that
|
|
134
|
+
* argument parsing, help text, and shell completion share one source of truth.
|
|
135
|
+
*/
|
|
136
|
+
export interface TestCommandFlag {
|
|
137
|
+
/** Long flag, e.g. `--glob.test`. */
|
|
138
|
+
name: string
|
|
139
|
+
/** Short alias, e.g. `-c`. */
|
|
140
|
+
alias?: string
|
|
141
|
+
type: 'boolean' | 'string'
|
|
142
|
+
/** Whether the flag may be repeated. */
|
|
143
|
+
multiple: boolean
|
|
144
|
+
/** Whether the flag's value completes as a file path. */
|
|
145
|
+
files: boolean
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const testCommandFlags: TestCommandFlag[] = Object.entries(
|
|
149
|
+
testCommandOptions as Record<
|
|
150
|
+
string,
|
|
151
|
+
{ type: 'boolean' | 'string'; multiple?: boolean; short?: string }
|
|
152
|
+
>,
|
|
153
|
+
).map(([name, option]) => ({
|
|
154
|
+
name: `--${name}`,
|
|
155
|
+
alias: option.short === undefined ? undefined : `-${option.short}`,
|
|
156
|
+
type: option.type,
|
|
157
|
+
multiple: option.multiple === true,
|
|
158
|
+
files: testCommandFlagMeta[name as keyof typeof testCommandOptions].files === true,
|
|
159
|
+
}))
|
|
5
160
|
|
|
6
161
|
export async function runTestCommand(argv: string[], context: CliContext): Promise<number> {
|
|
7
|
-
if (
|
|
8
|
-
process.stdout.write(
|
|
162
|
+
if (wantsHelp(argv)) {
|
|
163
|
+
process.stdout.write(getTestCommandHelpText())
|
|
9
164
|
return 0
|
|
10
165
|
}
|
|
11
166
|
|
|
12
167
|
try {
|
|
13
|
-
let { runRemixTest } = await import('@remix-run/test/cli')
|
|
14
|
-
|
|
168
|
+
let { remixTestPools, runRemixTest } = await import('@remix-run/test/cli')
|
|
169
|
+
let config = await context.loadConfig()
|
|
170
|
+
let options = resolveTestCommandOptions(argv, config.test, remixTestPools)
|
|
171
|
+
return await runRemixTest({ ...options, cwd: context.cwd })
|
|
15
172
|
} catch (error) {
|
|
16
|
-
let helpText = await getTestCommandHelpText(process.stderr)
|
|
17
173
|
process.stderr.write(
|
|
18
174
|
renderCliError(toCliError(error), {
|
|
19
|
-
helpText,
|
|
175
|
+
helpText: getTestCommandHelpText(process.stderr),
|
|
20
176
|
}),
|
|
21
177
|
)
|
|
22
178
|
return 1
|
|
23
179
|
}
|
|
24
180
|
}
|
|
25
181
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
182
|
+
export function resolveTestCommandOptions(
|
|
183
|
+
argv: string[],
|
|
184
|
+
config: RemixTestCommandConfig | undefined,
|
|
185
|
+
pools: readonly RemixTestPool[],
|
|
186
|
+
): RemixTestConfig {
|
|
187
|
+
let cliOptions = parseTestCommandArgs(argv, pools)
|
|
188
|
+
let configOptions = createTestOptionsFromConfig(config)
|
|
189
|
+
return mergeTestOptions(configOptions, cliOptions)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getTestCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
|
|
193
|
+
return formatHelpText(
|
|
194
|
+
{
|
|
195
|
+
description: 'Run tests for the current project.',
|
|
196
|
+
examples: [
|
|
197
|
+
'remix test',
|
|
198
|
+
'remix test app/**/*.test.ts',
|
|
199
|
+
'remix test --type server --concurrency 1',
|
|
200
|
+
'remix test --coverage',
|
|
201
|
+
'remix test --watch',
|
|
202
|
+
],
|
|
203
|
+
options: [
|
|
204
|
+
...Object.entries(testCommandOptions).map(([name, option]) => {
|
|
205
|
+
let meta = testCommandFlagMeta[name as keyof typeof testCommandOptions]
|
|
206
|
+
let short = 'short' in option ? `-${option.short}, ` : ''
|
|
207
|
+
let value = option.type === 'string' ? ` <${meta.valueHint}>` : ''
|
|
208
|
+
let repeatable = 'multiple' in option && option.multiple ? ' (repeatable)' : ''
|
|
209
|
+
let defaultValue = meta.default === undefined ? '' : ` (default: ${meta.default})`
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
description: `${meta.description}${repeatable}${defaultValue}`,
|
|
213
|
+
label: `${short}--${name}${value}`,
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
{ description: 'Use a custom Remix config file', label: '--config <path>' },
|
|
217
|
+
{ description: 'Show help', label: '-h, --help' },
|
|
218
|
+
{ description: 'Disable ANSI color output', label: '--no-color' },
|
|
219
|
+
],
|
|
220
|
+
usage: ['remix test [glob...] [options]'],
|
|
221
|
+
},
|
|
222
|
+
target,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function wantsHelp(argv: string[]): boolean {
|
|
227
|
+
// Everything after a bare `--` is a positional test file glob, not a flag
|
|
228
|
+
let separatorIndex = argv.indexOf('--')
|
|
229
|
+
let flags = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex)
|
|
230
|
+
return flags.includes('-h') || flags.includes('--help')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseTestCommandArgs(argv: string[], pools: readonly RemixTestPool[]) {
|
|
234
|
+
let parsed: ReturnType<typeof parseTestCommandArgsRaw>
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
parsed = parseTestCommandArgsRaw(argv)
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw toTestCommandUsageError(error)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let { positionals, tokens, values } = parsed
|
|
243
|
+
let coverage = createCoverageOptions(values, tokens)
|
|
244
|
+
let glob = compactObject({
|
|
245
|
+
browser: values['glob.browser'],
|
|
246
|
+
e2e: values['glob.e2e'],
|
|
247
|
+
exclude: values['glob.exclude'],
|
|
248
|
+
test: positionals.length > 0 ? positionals : values['glob.test'],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return compactObject({
|
|
252
|
+
browser: compactObject({
|
|
253
|
+
echo: resolveBooleanOption(tokens, 'browser.echo', 'no-browser.echo'),
|
|
254
|
+
open: resolveBooleanOption(tokens, 'browser.open', 'no-browser.open'),
|
|
255
|
+
}),
|
|
256
|
+
concurrency: optionalPositiveInteger(values.concurrency, '--concurrency'),
|
|
257
|
+
coverage,
|
|
258
|
+
glob,
|
|
259
|
+
only: values.only,
|
|
260
|
+
playwrightConfig: values.playwrightConfig,
|
|
261
|
+
pool: parsePool(values.pool, pools),
|
|
262
|
+
project: values.project,
|
|
263
|
+
quiet: resolveBooleanOption(tokens, 'quiet', 'no-quiet'),
|
|
264
|
+
reporter: values.reporter,
|
|
265
|
+
setup: values.setup,
|
|
266
|
+
type: values.type,
|
|
267
|
+
watch: resolveBooleanOption(tokens, 'watch', 'no-watch'),
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createCoverageOptions(
|
|
272
|
+
values: ReturnType<typeof parseTestCommandArgsRaw>['values'],
|
|
273
|
+
tokens: ReturnType<typeof parseTestCommandArgsRaw>['tokens'],
|
|
274
|
+
) {
|
|
275
|
+
let options = compactObject({
|
|
276
|
+
branches: optionalNumber(values['coverage.branches'], '--coverage.branches'),
|
|
277
|
+
dir: values['coverage.dir'],
|
|
278
|
+
exclude: values['coverage.exclude'],
|
|
279
|
+
functions: optionalNumber(values['coverage.functions'], '--coverage.functions'),
|
|
280
|
+
include: values['coverage.include'],
|
|
281
|
+
lines: optionalNumber(values['coverage.lines'], '--coverage.lines'),
|
|
282
|
+
statements: optionalNumber(values['coverage.statements'], '--coverage.statements'),
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
let enabled = resolveBooleanOption(tokens, 'coverage', 'no-coverage')
|
|
286
|
+
if (enabled === true) {
|
|
287
|
+
return options ?? true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (enabled === false) {
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Coverage settings without --coverage refine the config file's coverage
|
|
295
|
+
// options while leaving enablement up to the config file.
|
|
296
|
+
return options == null ? undefined : { ...options, enabled: 'inherit' as const }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function parseTestCommandArgsRaw(argv: string[]) {
|
|
300
|
+
return parseArgs({
|
|
301
|
+
allowPositionals: true,
|
|
302
|
+
args: argv,
|
|
303
|
+
options: testCommandOptions,
|
|
304
|
+
tokens: true,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function resolveBooleanOption(
|
|
309
|
+
tokens: ReturnType<typeof parseTestCommandArgsRaw>['tokens'],
|
|
310
|
+
enabledName: string,
|
|
311
|
+
disabledName: string,
|
|
312
|
+
): boolean | undefined {
|
|
313
|
+
let value: boolean | undefined
|
|
314
|
+
|
|
315
|
+
for (let token of tokens) {
|
|
316
|
+
if (token.kind !== 'option') continue
|
|
317
|
+
if (token.name === enabledName) value = true
|
|
318
|
+
if (token.name === disabledName) value = false
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return value
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createTestOptionsFromConfig(config: RemixTestCommandConfig | undefined): RemixTestConfig {
|
|
325
|
+
if (config === undefined) return {}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
browser:
|
|
329
|
+
config.playwright === undefined
|
|
330
|
+
? undefined
|
|
331
|
+
: {
|
|
332
|
+
echo: config.playwright.echo,
|
|
333
|
+
open: config.playwright.open,
|
|
334
|
+
},
|
|
335
|
+
concurrency: config.concurrency,
|
|
336
|
+
coverage: config.coverage,
|
|
337
|
+
glob: {
|
|
338
|
+
browser: config.browserFiles,
|
|
339
|
+
e2e: config.e2eFiles,
|
|
340
|
+
exclude: config.exclude,
|
|
341
|
+
test: config.files,
|
|
342
|
+
},
|
|
343
|
+
only: config.only,
|
|
344
|
+
playwrightConfig: config.playwright?.configFile,
|
|
345
|
+
pool: config.pool,
|
|
346
|
+
project: config.playwright?.projects,
|
|
347
|
+
quiet: config.quiet,
|
|
348
|
+
reporter: config.reporter,
|
|
349
|
+
setup: config.setup,
|
|
350
|
+
type: config.type,
|
|
351
|
+
watch: config.watch,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function mergeTestOptions(
|
|
356
|
+
config: RemixTestConfig,
|
|
357
|
+
cli: RemixTestConfig | undefined,
|
|
358
|
+
): RemixTestConfig {
|
|
359
|
+
if (cli === undefined) return config
|
|
360
|
+
|
|
361
|
+
// CLI options are compacted (no undefined values), so spreading them over
|
|
362
|
+
// the config gives CLI input precedence; nested objects merge by field.
|
|
363
|
+
return {
|
|
364
|
+
...config,
|
|
365
|
+
...cli,
|
|
366
|
+
browser: mergeObject(config.browser, cli.browser),
|
|
367
|
+
coverage: mergeCoverage(config.coverage, cli.coverage),
|
|
368
|
+
glob: mergeObject(config.glob, cli.glob),
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function mergeObject<value extends object>(
|
|
373
|
+
config: value | undefined,
|
|
374
|
+
cli: value | undefined,
|
|
375
|
+
): value | undefined {
|
|
376
|
+
if (config === undefined) return cli
|
|
377
|
+
if (cli === undefined) return config
|
|
378
|
+
return { ...config, ...cli }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function mergeCoverage(
|
|
382
|
+
config: RemixTestConfig['coverage'],
|
|
383
|
+
cli: RemixTestConfig['coverage'],
|
|
384
|
+
): RemixTestConfig['coverage'] {
|
|
385
|
+
if (cli === undefined) return config
|
|
386
|
+
if (cli === false) return false
|
|
387
|
+
|
|
388
|
+
let configOptions = typeof config === 'object' && config !== null ? config : {}
|
|
389
|
+
if (cli === true) return { ...configOptions, enabled: true }
|
|
390
|
+
|
|
391
|
+
let enabled =
|
|
392
|
+
cli.enabled === 'inherit'
|
|
393
|
+
? typeof config === 'boolean'
|
|
394
|
+
? config
|
|
395
|
+
: config === undefined
|
|
396
|
+
? false
|
|
397
|
+
: (config.enabled ?? true)
|
|
398
|
+
: cli.enabled
|
|
399
|
+
|
|
400
|
+
return { ...configOptions, ...cli, enabled }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function optionalNumber(value: string | undefined, flag: string): number | undefined {
|
|
404
|
+
if (value === undefined) {
|
|
405
|
+
return undefined
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let parsed = Number(value)
|
|
409
|
+
if (value.trim() === '' || Number.isNaN(parsed)) {
|
|
410
|
+
throw invalidOptionValue(`Invalid ${flag} value "${value}". Expected a number`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return parsed
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function optionalPositiveInteger(value: string | undefined, flag: string): number | undefined {
|
|
417
|
+
let parsed = optionalNumber(value, flag)
|
|
418
|
+
if (parsed !== undefined && (!Number.isInteger(parsed) || parsed < 1)) {
|
|
419
|
+
throw invalidOptionValue(`Invalid ${flag} value "${value}". Expected a positive integer`)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return parsed
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parsePool(
|
|
426
|
+
value: string | undefined,
|
|
427
|
+
pools: readonly RemixTestPool[],
|
|
428
|
+
): RemixTestPool | undefined {
|
|
429
|
+
if (value === undefined || (pools as readonly string[]).includes(value)) {
|
|
430
|
+
return value as RemixTestPool | undefined
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
throw invalidOptionValue(
|
|
434
|
+
`Unsupported test pool "${value}". Supported pools are: ${pools.join(', ')}`,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function compactObject<shape extends Record<string, unknown>>(value: shape): shape | undefined {
|
|
439
|
+
for (let key in value) {
|
|
440
|
+
if (value[key] === undefined) delete value[key]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return Object.keys(value).length === 0 ? undefined : value
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function toTestCommandUsageError(error: unknown): unknown {
|
|
447
|
+
if (!isErrorWithCode(error)) {
|
|
448
|
+
return error
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
|
|
452
|
+
let option = error.message.match(/'([^']+)'/)?.[1]
|
|
453
|
+
return unknownArgument(option ?? error.message)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (error.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE') {
|
|
457
|
+
// parseArgs reports a flag with no value as an invalid-value error, e.g.
|
|
458
|
+
// "Option '--config <value>' argument missing"; surface it as missing.
|
|
459
|
+
let missing = error.message.match(/^Option '(.+?) <value>' argument missing/)
|
|
460
|
+
if (missing !== null) {
|
|
461
|
+
return missingOptionValue(missing[1])
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return invalidOptionValue(error.message)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return error
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isErrorWithCode(error: unknown): error is Error & { code: string } {
|
|
471
|
+
return error instanceof Error && 'code' in error && typeof error.code === 'string'
|
|
31
472
|
}
|