@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
@@ -1,6 +1,6 @@
1
1
  import * as process from 'node:process';
2
- import { renderCliError, toCliError, unknownHelpTopic } from "../errors.js";
3
- import { formatHelpText } from "../help-text.js";
2
+ import { renderCliError, toCliError, unknownHelpTopic } from '../errors.js';
3
+ import { formatHelpText } from '../help-text.js';
4
4
  export async function runHelpCommand(argv) {
5
5
  if (argv.includes('-h') || argv.includes('--help')) {
6
6
  process.stdout.write(getHelpCommandHelpText());
@@ -21,6 +21,7 @@ export function getCliHelpText(target = process.stdout) {
21
21
  { description: 'Print shell completion scripts for Remix', label: 'completion' },
22
22
  { description: 'Show help for Remix commands', label: 'help [command]' },
23
23
  { description: 'Create a new Remix project', label: 'new <name>' },
24
+ { description: 'Manage the current app database', label: 'db <command>' },
24
25
  { description: 'Check project health for the current project', label: 'doctor' },
25
26
  { description: 'Show the route tree for the current project', label: 'routes' },
26
27
  { description: 'Run tests for the current project', label: 'test [glob]' },
@@ -31,6 +32,7 @@ export function getCliHelpText(target = process.stdout) {
31
32
  'remix help',
32
33
  'remix help completion',
33
34
  'remix help doctor',
35
+ 'remix db status',
34
36
  'remix doctor',
35
37
  'remix new my-remix-app',
36
38
  'remix new my-remix-app --app-name "My Remix App"',
@@ -39,6 +41,7 @@ export function getCliHelpText(target = process.stdout) {
39
41
  'remix version',
40
42
  ],
41
43
  options: [
44
+ { description: 'Use a custom Remix config file', label: '--config <path>' },
42
45
  { description: 'Show help', label: '-h, --help' },
43
46
  { description: 'Disable ANSI color output', label: '--no-color' },
44
47
  { description: 'Show version', label: '-v, --version' },
@@ -52,6 +55,7 @@ export function getHelpCommandHelpText(target = process.stdout) {
52
55
  examples: [
53
56
  'remix help',
54
57
  'remix help completion',
58
+ 'remix help db',
55
59
  'remix help doctor',
56
60
  'remix help new',
57
61
  'remix help routes',
@@ -70,27 +74,31 @@ async function getCommandHelpText(argv) {
70
74
  return rest.length === 0 ? getHelpCommandHelpText() : getNestedHelpText(command, rest);
71
75
  }
72
76
  if (command === 'new' && rest.length === 0) {
73
- let { getNewCommandHelpText } = await import("./new.js");
77
+ let { getNewCommandHelpText } = await import('./new.js');
74
78
  return getNewCommandHelpText();
75
79
  }
76
80
  if (command === 'completion' && rest.length === 0) {
77
- let { getCompletionCommandHelpText } = await import("./completion.js");
81
+ let { getCompletionCommandHelpText } = await import('./completion.js');
78
82
  return getCompletionCommandHelpText();
79
83
  }
80
84
  if (command === 'doctor' && rest.length === 0) {
81
- let { getDoctorCommandHelpText } = await import("./doctor.js");
85
+ let { getDoctorCommandHelpText } = await import('./doctor.js');
82
86
  return getDoctorCommandHelpText();
83
87
  }
88
+ if (command === 'db' && rest.length === 0) {
89
+ let { getDbCommandHelpText } = await import('./db.js');
90
+ return getDbCommandHelpText();
91
+ }
84
92
  if (command === 'routes' && rest.length === 0) {
85
- let { getRoutesCommandHelpText } = await import("./routes.js");
93
+ let { getRoutesCommandHelpText } = await import('./routes.js');
86
94
  return getRoutesCommandHelpText();
87
95
  }
88
96
  if (command === 'test' && rest.length === 0) {
89
- let { getTestCommandHelpText } = await import("./test.js");
90
- return await getTestCommandHelpText();
97
+ let { getTestCommandHelpText } = await import('./test.js');
98
+ return getTestCommandHelpText();
91
99
  }
92
100
  if (command === 'version' && rest.length === 0) {
93
- let { getVersionCommandHelpText } = await import("./version.js");
101
+ let { getVersionCommandHelpText } = await import('./version.js');
94
102
  return getVersionCommandHelpText();
95
103
  }
96
104
  throw unknownHelpTopic(argv.join(' '));
@@ -1,10 +1,10 @@
1
1
  import * as process from 'node:process';
2
- import { bootstrapProject } from "../bootstrap-project.js";
3
- import { renderCliError, missingTargetDirectory, toCliError } from "../errors.js";
4
- import { getDisplayPath } from "../display-path.js";
5
- import { formatHelpText } from "../help-text.js";
6
- import { parseArgs } from "../parse-args.js";
7
- import { createCommandReporter, createStepProgressReporter, } from "../reporter.js";
2
+ import { bootstrapProject } from '../bootstrap-project.js';
3
+ import { renderCliError, missingTargetDirectory, toCliError } from '../errors.js';
4
+ import { getDisplayPath } from '../display-path.js';
5
+ import { formatHelpText } from '../help-text.js';
6
+ import { parseArgs } from '../parse-args.js';
7
+ import { createCommandReporter, createStepProgressReporter, } from '../reporter.js';
8
8
  const NEW_PROGRESS_LABELS = {
9
9
  'finalize-package-json': 'Finalize package.json',
10
10
  'generate-scaffold-files': 'Generate scaffold files',
@@ -1,10 +1,10 @@
1
1
  import * as process from 'node:process';
2
- import { invalidFlagCombination, renderCliError, toCliError } from "../errors.js";
3
- import { formatHelpText } from "../help-text.js";
4
- import { parseArgs } from "../parse-args.js";
5
- import { createCommandReporter } from "../reporter.js";
6
- import { loadRouteMap } from "../route-map.js";
7
- import { lightRed } from "../terminal.js";
2
+ import { invalidFlagCombination, renderCliError, toCliError } from '../errors.js';
3
+ import { formatHelpText } from '../help-text.js';
4
+ import { parseArgs } from '../parse-args.js';
5
+ import { createCommandReporter } from '../reporter.js';
6
+ import { loadRouteMap } from '../route-map.js';
7
+ import { lightRed } from '../terminal.js';
8
8
  const ACTIONS_PATH_PREFIX = 'app/actions/';
9
9
  export async function runRoutesCommand(argv, context) {
10
10
  if (argv.includes('-h') || argv.includes('--help')) {
@@ -1,4 +1,23 @@
1
+ import type { RemixTestConfig, RemixTestPool } from '@remix-run/test/cli';
1
2
  import type { CliContext } from '../cli-context.ts';
3
+ import type { RemixTestCommandConfig } from '../remix-config.ts';
4
+ /**
5
+ * Flag descriptors for `remix test`, derived from the parse table so that
6
+ * argument parsing, help text, and shell completion share one source of truth.
7
+ */
8
+ export interface TestCommandFlag {
9
+ /** Long flag, e.g. `--glob.test`. */
10
+ name: string;
11
+ /** Short alias, e.g. `-c`. */
12
+ alias?: string;
13
+ type: 'boolean' | 'string';
14
+ /** Whether the flag may be repeated. */
15
+ multiple: boolean;
16
+ /** Whether the flag's value completes as a file path. */
17
+ files: boolean;
18
+ }
19
+ export declare const testCommandFlags: TestCommandFlag[];
2
20
  export declare function runTestCommand(argv: string[], context: CliContext): Promise<number>;
3
- export declare function getTestCommandHelpText(target?: NodeJS.WriteStream): Promise<string>;
21
+ export declare function resolveTestCommandOptions(argv: string[], config: RemixTestCommandConfig | undefined, pools: readonly RemixTestPool[]): RemixTestConfig;
22
+ export declare function getTestCommandHelpText(target?: NodeJS.WriteStream): string;
4
23
  //# sourceMappingURL=test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/test.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAGnD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBzF;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,GAAE,MAAM,CAAC,WAA4B,GAC1C,OAAO,CAAC,MAAM,CAAC,CAGjB"}
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/test.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AASnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAsHhE;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC1B,wCAAwC;IACxC,QAAQ,EAAE,OAAO,CAAA;IACjB,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAA;CACf;AAED,eAAO,MAAM,gBAAgB,EAAE,eAAe,EAW3C,CAAA;AAEH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBzF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,sBAAsB,GAAG,SAAS,EAC1C,KAAK,EAAE,SAAS,aAAa,EAAE,GAC9B,eAAe,CAIjB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAgC1F"}
@@ -1,23 +1,366 @@
1
1
  import * as process from 'node:process';
2
- import { renderCliError, toCliError } from "../errors.js";
2
+ import { parseArgs } from 'node:util';
3
+ import { invalidOptionValue, missingOptionValue, renderCliError, toCliError, unknownArgument, } from '../errors.js';
4
+ import { formatHelpText } from '../help-text.js';
5
+ const testCommandOptions = {
6
+ 'browser.echo': { type: 'boolean' },
7
+ 'no-browser.echo': { type: 'boolean' },
8
+ 'browser.open': { type: 'boolean' },
9
+ 'no-browser.open': { type: 'boolean' },
10
+ 'glob.browser': { type: 'string', multiple: true },
11
+ 'glob.e2e': { type: 'string', multiple: true },
12
+ 'glob.exclude': { type: 'string', multiple: true },
13
+ 'glob.test': { type: 'string', multiple: true },
14
+ concurrency: { type: 'string', short: 'c' },
15
+ coverage: { type: 'boolean' },
16
+ 'no-coverage': { type: 'boolean' },
17
+ 'coverage.dir': { type: 'string' },
18
+ 'coverage.include': { type: 'string', multiple: true },
19
+ 'coverage.exclude': { type: 'string', multiple: true },
20
+ 'coverage.branches': { type: 'string' },
21
+ 'coverage.functions': { type: 'string' },
22
+ 'coverage.lines': { type: 'string' },
23
+ 'coverage.statements': { type: 'string' },
24
+ setup: { type: 'string', short: 's' },
25
+ playwrightConfig: { type: 'string' },
26
+ project: { type: 'string', short: 'p', multiple: true },
27
+ pool: { type: 'string' },
28
+ quiet: { type: 'boolean', short: 'q' },
29
+ 'no-quiet': { type: 'boolean' },
30
+ only: { type: 'string', multiple: true },
31
+ reporter: { type: 'string', short: 'r' },
32
+ type: { type: 'string', short: 't', multiple: true },
33
+ watch: { type: 'boolean', short: 'w' },
34
+ 'no-watch': { type: 'boolean' },
35
+ };
36
+ // The annotation keeps this table in lockstep with `testCommandOptions`: adding
37
+ // or removing a flag without updating its metadata is a compile error.
38
+ const testCommandFlagMeta = {
39
+ 'browser.echo': { description: 'Echo browser console output' },
40
+ 'no-browser.echo': { description: 'Do not echo browser console output' },
41
+ 'browser.open': { description: 'Open the browser after tests finish' },
42
+ 'no-browser.open': { description: 'Do not open the browser after tests finish' },
43
+ 'glob.browser': {
44
+ description: 'Glob pattern for browser test files',
45
+ valueHint: 'glob',
46
+ files: true,
47
+ },
48
+ 'glob.e2e': { description: 'Glob pattern for E2E test files', valueHint: 'glob', files: true },
49
+ 'glob.exclude': {
50
+ description: 'Glob pattern to exclude from discovery',
51
+ valueHint: 'glob',
52
+ files: true,
53
+ },
54
+ 'glob.test': {
55
+ description: 'Glob pattern for all test files',
56
+ valueHint: 'glob',
57
+ files: true,
58
+ default: '**/*.test{,.e2e,.browser}.{ts,tsx}',
59
+ },
60
+ concurrency: {
61
+ description: 'Maximum concurrent test workers',
62
+ valueHint: 'count',
63
+ default: 'os.availableParallelism()',
64
+ },
65
+ coverage: { description: 'Collect test coverage', default: 'false' },
66
+ 'no-coverage': { description: 'Do not collect test coverage' },
67
+ 'coverage.dir': {
68
+ description: 'Coverage report output directory',
69
+ valueHint: 'path',
70
+ default: '.coverage',
71
+ },
72
+ 'coverage.include': { description: 'Coverage inclusion glob', valueHint: 'glob' },
73
+ 'coverage.exclude': { description: 'Coverage exclusion glob', valueHint: 'glob' },
74
+ 'coverage.branches': { description: 'Minimum branch coverage percentage', valueHint: 'percent' },
75
+ 'coverage.functions': {
76
+ description: 'Minimum function coverage percentage',
77
+ valueHint: 'percent',
78
+ },
79
+ 'coverage.lines': { description: 'Minimum line coverage percentage', valueHint: 'percent' },
80
+ 'coverage.statements': {
81
+ description: 'Minimum statement coverage percentage',
82
+ valueHint: 'percent',
83
+ },
84
+ setup: { description: 'Path to a setup module', valueHint: 'path', files: true },
85
+ playwrightConfig: {
86
+ description: 'Path to a Playwright config file',
87
+ valueHint: 'path',
88
+ files: true,
89
+ },
90
+ project: { description: 'Playwright project name', valueHint: 'name' },
91
+ pool: { description: 'Worker pool: forks or threads', valueHint: 'pool', default: 'forks' },
92
+ quiet: { description: 'Do not print skipped tests' },
93
+ 'no-quiet': { description: 'Print skipped tests' },
94
+ only: { description: 'Test name pattern', valueHint: 'pattern' },
95
+ reporter: {
96
+ description: 'Reporter: spec, files, tap, or dot',
97
+ valueHint: 'name',
98
+ default: 'spec',
99
+ },
100
+ type: {
101
+ description: 'Test type: server, browser, or e2e',
102
+ valueHint: 'type',
103
+ default: 'server, browser, e2e',
104
+ },
105
+ watch: { description: 'Re-run tests when files change' },
106
+ 'no-watch': { description: 'Do not re-run tests when files change' },
107
+ };
108
+ export const testCommandFlags = Object.entries(testCommandOptions).map(([name, option]) => ({
109
+ name: `--${name}`,
110
+ alias: option.short === undefined ? undefined : `-${option.short}`,
111
+ type: option.type,
112
+ multiple: option.multiple === true,
113
+ files: testCommandFlagMeta[name].files === true,
114
+ }));
3
115
  export async function runTestCommand(argv, context) {
4
- if (argv.includes('-h') || argv.includes('--help')) {
5
- process.stdout.write(`${await getTestCommandHelpText()}\n`);
116
+ if (wantsHelp(argv)) {
117
+ process.stdout.write(getTestCommandHelpText());
6
118
  return 0;
7
119
  }
8
120
  try {
9
- let { runRemixTest } = await import('@remix-run/test/cli');
10
- return await runRemixTest({ argv, cwd: context.cwd });
121
+ let { remixTestPools, runRemixTest } = await import('@remix-run/test/cli');
122
+ let config = await context.loadConfig();
123
+ let options = resolveTestCommandOptions(argv, config.test, remixTestPools);
124
+ return await runRemixTest({ ...options, cwd: context.cwd });
11
125
  }
12
126
  catch (error) {
13
- let helpText = await getTestCommandHelpText(process.stderr);
14
127
  process.stderr.write(renderCliError(toCliError(error), {
15
- helpText,
128
+ helpText: getTestCommandHelpText(process.stderr),
16
129
  }));
17
130
  return 1;
18
131
  }
19
132
  }
20
- export async function getTestCommandHelpText(target = process.stdout) {
21
- let { getRemixTestHelpText } = await import('@remix-run/test/cli');
22
- return getRemixTestHelpText(target);
133
+ export function resolveTestCommandOptions(argv, config, pools) {
134
+ let cliOptions = parseTestCommandArgs(argv, pools);
135
+ let configOptions = createTestOptionsFromConfig(config);
136
+ return mergeTestOptions(configOptions, cliOptions);
137
+ }
138
+ export function getTestCommandHelpText(target = process.stdout) {
139
+ return formatHelpText({
140
+ description: 'Run tests for the current project.',
141
+ examples: [
142
+ 'remix test',
143
+ 'remix test app/**/*.test.ts',
144
+ 'remix test --type server --concurrency 1',
145
+ 'remix test --coverage',
146
+ 'remix test --watch',
147
+ ],
148
+ options: [
149
+ ...Object.entries(testCommandOptions).map(([name, option]) => {
150
+ let meta = testCommandFlagMeta[name];
151
+ let short = 'short' in option ? `-${option.short}, ` : '';
152
+ let value = option.type === 'string' ? ` <${meta.valueHint}>` : '';
153
+ let repeatable = 'multiple' in option && option.multiple ? ' (repeatable)' : '';
154
+ let defaultValue = meta.default === undefined ? '' : ` (default: ${meta.default})`;
155
+ return {
156
+ description: `${meta.description}${repeatable}${defaultValue}`,
157
+ label: `${short}--${name}${value}`,
158
+ };
159
+ }),
160
+ { description: 'Use a custom Remix config file', label: '--config <path>' },
161
+ { description: 'Show help', label: '-h, --help' },
162
+ { description: 'Disable ANSI color output', label: '--no-color' },
163
+ ],
164
+ usage: ['remix test [glob...] [options]'],
165
+ }, target);
166
+ }
167
+ function wantsHelp(argv) {
168
+ // Everything after a bare `--` is a positional test file glob, not a flag
169
+ let separatorIndex = argv.indexOf('--');
170
+ let flags = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
171
+ return flags.includes('-h') || flags.includes('--help');
172
+ }
173
+ function parseTestCommandArgs(argv, pools) {
174
+ let parsed;
175
+ try {
176
+ parsed = parseTestCommandArgsRaw(argv);
177
+ }
178
+ catch (error) {
179
+ throw toTestCommandUsageError(error);
180
+ }
181
+ let { positionals, tokens, values } = parsed;
182
+ let coverage = createCoverageOptions(values, tokens);
183
+ let glob = compactObject({
184
+ browser: values['glob.browser'],
185
+ e2e: values['glob.e2e'],
186
+ exclude: values['glob.exclude'],
187
+ test: positionals.length > 0 ? positionals : values['glob.test'],
188
+ });
189
+ return compactObject({
190
+ browser: compactObject({
191
+ echo: resolveBooleanOption(tokens, 'browser.echo', 'no-browser.echo'),
192
+ open: resolveBooleanOption(tokens, 'browser.open', 'no-browser.open'),
193
+ }),
194
+ concurrency: optionalPositiveInteger(values.concurrency, '--concurrency'),
195
+ coverage,
196
+ glob,
197
+ only: values.only,
198
+ playwrightConfig: values.playwrightConfig,
199
+ pool: parsePool(values.pool, pools),
200
+ project: values.project,
201
+ quiet: resolveBooleanOption(tokens, 'quiet', 'no-quiet'),
202
+ reporter: values.reporter,
203
+ setup: values.setup,
204
+ type: values.type,
205
+ watch: resolveBooleanOption(tokens, 'watch', 'no-watch'),
206
+ });
207
+ }
208
+ function createCoverageOptions(values, tokens) {
209
+ let options = compactObject({
210
+ branches: optionalNumber(values['coverage.branches'], '--coverage.branches'),
211
+ dir: values['coverage.dir'],
212
+ exclude: values['coverage.exclude'],
213
+ functions: optionalNumber(values['coverage.functions'], '--coverage.functions'),
214
+ include: values['coverage.include'],
215
+ lines: optionalNumber(values['coverage.lines'], '--coverage.lines'),
216
+ statements: optionalNumber(values['coverage.statements'], '--coverage.statements'),
217
+ });
218
+ let enabled = resolveBooleanOption(tokens, 'coverage', 'no-coverage');
219
+ if (enabled === true) {
220
+ return options ?? true;
221
+ }
222
+ if (enabled === false) {
223
+ return false;
224
+ }
225
+ // Coverage settings without --coverage refine the config file's coverage
226
+ // options while leaving enablement up to the config file.
227
+ return options == null ? undefined : { ...options, enabled: 'inherit' };
228
+ }
229
+ function parseTestCommandArgsRaw(argv) {
230
+ return parseArgs({
231
+ allowPositionals: true,
232
+ args: argv,
233
+ options: testCommandOptions,
234
+ tokens: true,
235
+ });
236
+ }
237
+ function resolveBooleanOption(tokens, enabledName, disabledName) {
238
+ let value;
239
+ for (let token of tokens) {
240
+ if (token.kind !== 'option')
241
+ continue;
242
+ if (token.name === enabledName)
243
+ value = true;
244
+ if (token.name === disabledName)
245
+ value = false;
246
+ }
247
+ return value;
248
+ }
249
+ function createTestOptionsFromConfig(config) {
250
+ if (config === undefined)
251
+ return {};
252
+ return {
253
+ browser: config.playwright === undefined
254
+ ? undefined
255
+ : {
256
+ echo: config.playwright.echo,
257
+ open: config.playwright.open,
258
+ },
259
+ concurrency: config.concurrency,
260
+ coverage: config.coverage,
261
+ glob: {
262
+ browser: config.browserFiles,
263
+ e2e: config.e2eFiles,
264
+ exclude: config.exclude,
265
+ test: config.files,
266
+ },
267
+ only: config.only,
268
+ playwrightConfig: config.playwright?.configFile,
269
+ pool: config.pool,
270
+ project: config.playwright?.projects,
271
+ quiet: config.quiet,
272
+ reporter: config.reporter,
273
+ setup: config.setup,
274
+ type: config.type,
275
+ watch: config.watch,
276
+ };
277
+ }
278
+ function mergeTestOptions(config, cli) {
279
+ if (cli === undefined)
280
+ return config;
281
+ // CLI options are compacted (no undefined values), so spreading them over
282
+ // the config gives CLI input precedence; nested objects merge by field.
283
+ return {
284
+ ...config,
285
+ ...cli,
286
+ browser: mergeObject(config.browser, cli.browser),
287
+ coverage: mergeCoverage(config.coverage, cli.coverage),
288
+ glob: mergeObject(config.glob, cli.glob),
289
+ };
290
+ }
291
+ function mergeObject(config, cli) {
292
+ if (config === undefined)
293
+ return cli;
294
+ if (cli === undefined)
295
+ return config;
296
+ return { ...config, ...cli };
297
+ }
298
+ function mergeCoverage(config, cli) {
299
+ if (cli === undefined)
300
+ return config;
301
+ if (cli === false)
302
+ return false;
303
+ let configOptions = typeof config === 'object' && config !== null ? config : {};
304
+ if (cli === true)
305
+ return { ...configOptions, enabled: true };
306
+ let enabled = cli.enabled === 'inherit'
307
+ ? typeof config === 'boolean'
308
+ ? config
309
+ : config === undefined
310
+ ? false
311
+ : (config.enabled ?? true)
312
+ : cli.enabled;
313
+ return { ...configOptions, ...cli, enabled };
314
+ }
315
+ function optionalNumber(value, flag) {
316
+ if (value === undefined) {
317
+ return undefined;
318
+ }
319
+ let parsed = Number(value);
320
+ if (value.trim() === '' || Number.isNaN(parsed)) {
321
+ throw invalidOptionValue(`Invalid ${flag} value "${value}". Expected a number`);
322
+ }
323
+ return parsed;
324
+ }
325
+ function optionalPositiveInteger(value, flag) {
326
+ let parsed = optionalNumber(value, flag);
327
+ if (parsed !== undefined && (!Number.isInteger(parsed) || parsed < 1)) {
328
+ throw invalidOptionValue(`Invalid ${flag} value "${value}". Expected a positive integer`);
329
+ }
330
+ return parsed;
331
+ }
332
+ function parsePool(value, pools) {
333
+ if (value === undefined || pools.includes(value)) {
334
+ return value;
335
+ }
336
+ throw invalidOptionValue(`Unsupported test pool "${value}". Supported pools are: ${pools.join(', ')}`);
337
+ }
338
+ function compactObject(value) {
339
+ for (let key in value) {
340
+ if (value[key] === undefined)
341
+ delete value[key];
342
+ }
343
+ return Object.keys(value).length === 0 ? undefined : value;
344
+ }
345
+ function toTestCommandUsageError(error) {
346
+ if (!isErrorWithCode(error)) {
347
+ return error;
348
+ }
349
+ if (error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
350
+ let option = error.message.match(/'([^']+)'/)?.[1];
351
+ return unknownArgument(option ?? error.message);
352
+ }
353
+ if (error.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE') {
354
+ // parseArgs reports a flag with no value as an invalid-value error, e.g.
355
+ // "Option '--config <value>' argument missing"; surface it as missing.
356
+ let missing = error.message.match(/^Option '(.+?) <value>' argument missing/);
357
+ if (missing !== null) {
358
+ return missingOptionValue(missing[1]);
359
+ }
360
+ return invalidOptionValue(error.message);
361
+ }
362
+ return error;
363
+ }
364
+ function isErrorWithCode(error) {
365
+ return error instanceof Error && 'code' in error && typeof error.code === 'string';
23
366
  }
@@ -1,7 +1,7 @@
1
1
  import * as process from 'node:process';
2
- import { remixVersionUnavailable, renderCliError, toCliError } from "../errors.js";
3
- import { formatHelpText } from "../help-text.js";
4
- import { parseArgs } from "../parse-args.js";
2
+ import { remixVersionUnavailable, renderCliError, toCliError } from '../errors.js';
3
+ import { formatHelpText } from '../help-text.js';
4
+ import { parseArgs } from '../parse-args.js';
5
5
  export async function runVersionCommand(argv, context) {
6
6
  if (argv.includes('-h') || argv.includes('--help')) {
7
7
  process.stdout.write(getVersionCommandHelpText());
@@ -2,7 +2,7 @@ export interface CompletionResult {
2
2
  mode: 'files' | 'none' | 'values';
3
3
  values?: string[];
4
4
  }
5
- declare const COMPLETION_SHELLS: readonly ["bash", "zsh"];
5
+ declare const COMPLETION_SHELLS: readonly ['bash', 'zsh'];
6
6
  export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
7
7
  export declare function isCompletionShell(value: string): value is CompletionShell;
8
8
  export declare function getCompletionResult(words: string[], currentIndex: number): CompletionResult;
@@ -1 +1 @@
1
- {"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../../src/lib/completion.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;IACjC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,QAAA,MAAM,iBAAiB,0BAA2B,CAAA;AAIlD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEhE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,eAAe,CAEzE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,gBAAgB,CAM3F;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAQvE;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CA4E5C"}
1
+ {"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../../src/lib/completion.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;IACjC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,QAAA,MAAM,iBAAiB,YAAI,MAAM,EAAE,KAAK,CAAU,CAAA;AA6ClD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEhE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,eAAe,CAEzE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,gBAAgB,CAU3F;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAQvE;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAyF5C"}