@react-native-harness/cli 1.1.0 → 1.2.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 (116) hide show
  1. package/dist/__tests__/platform-commands.test.d.ts +2 -0
  2. package/dist/__tests__/platform-commands.test.d.ts.map +1 -0
  3. package/dist/__tests__/platform-commands.test.js +207 -0
  4. package/dist/bundlers/metro.d.ts +5 -0
  5. package/dist/bundlers/metro.d.ts.map +1 -0
  6. package/dist/bundlers/metro.js +71 -0
  7. package/dist/bundlers/webpack.d.ts +2 -0
  8. package/dist/bundlers/webpack.d.ts.map +1 -0
  9. package/dist/bundlers/webpack.js +49 -0
  10. package/dist/commands/test.d.ts +3 -0
  11. package/dist/commands/test.d.ts.map +1 -0
  12. package/dist/commands/test.js +140 -0
  13. package/dist/discovery/index.d.ts +3 -0
  14. package/dist/discovery/index.d.ts.map +1 -0
  15. package/dist/discovery/index.js +1 -0
  16. package/dist/discovery/testDiscovery.d.ts +11 -0
  17. package/dist/discovery/testDiscovery.d.ts.map +1 -0
  18. package/dist/discovery/testDiscovery.js +29 -0
  19. package/dist/errors/appNotInstalledError.d.ts +7 -0
  20. package/dist/errors/appNotInstalledError.d.ts.map +1 -0
  21. package/dist/errors/appNotInstalledError.js +12 -0
  22. package/dist/errors/bridgeTimeoutError.d.ts +7 -0
  23. package/dist/errors/bridgeTimeoutError.d.ts.map +1 -0
  24. package/dist/errors/bridgeTimeoutError.js +12 -0
  25. package/dist/errors/errorHandler.d.ts +2 -0
  26. package/dist/errors/errorHandler.d.ts.map +1 -0
  27. package/dist/errors/errorHandler.js +152 -0
  28. package/dist/errors/errors.d.ts +45 -0
  29. package/dist/errors/errors.d.ts.map +1 -0
  30. package/dist/errors/errors.js +89 -0
  31. package/dist/index.js +113 -6
  32. package/dist/jest.d.ts +2 -0
  33. package/dist/jest.d.ts.map +1 -0
  34. package/dist/jest.js +7 -0
  35. package/dist/platform-commands.d.ts +18 -0
  36. package/dist/platform-commands.d.ts.map +1 -0
  37. package/dist/platform-commands.js +84 -0
  38. package/dist/platforms/android/build.d.ts +5 -0
  39. package/dist/platforms/android/build.d.ts.map +1 -0
  40. package/dist/platforms/android/build.js +29 -0
  41. package/dist/platforms/android/device.d.ts +5 -0
  42. package/dist/platforms/android/device.d.ts.map +1 -0
  43. package/dist/platforms/android/device.js +36 -0
  44. package/dist/platforms/android/emulator.d.ts +10 -0
  45. package/dist/platforms/android/emulator.d.ts.map +1 -0
  46. package/dist/platforms/android/emulator.js +116 -0
  47. package/dist/platforms/android/index.d.ts +4 -0
  48. package/dist/platforms/android/index.d.ts.map +1 -0
  49. package/dist/platforms/android/index.js +56 -0
  50. package/dist/platforms/ios/build.d.ts +7 -0
  51. package/dist/platforms/ios/build.d.ts.map +1 -0
  52. package/dist/platforms/ios/build.js +48 -0
  53. package/dist/platforms/ios/device.d.ts +11 -0
  54. package/dist/platforms/ios/device.d.ts.map +1 -0
  55. package/dist/platforms/ios/device.js +51 -0
  56. package/dist/platforms/ios/index.d.ts +4 -0
  57. package/dist/platforms/ios/index.d.ts.map +1 -0
  58. package/dist/platforms/ios/index.js +43 -0
  59. package/dist/platforms/ios/simulator.d.ts +11 -0
  60. package/dist/platforms/ios/simulator.d.ts.map +1 -0
  61. package/dist/platforms/ios/simulator.js +129 -0
  62. package/dist/platforms/platform-adapter.d.ts +10 -0
  63. package/dist/platforms/platform-adapter.d.ts.map +1 -0
  64. package/dist/platforms/platform-adapter.js +1 -0
  65. package/dist/platforms/platform-registry.d.ts +3 -0
  66. package/dist/platforms/platform-registry.d.ts.map +1 -0
  67. package/dist/platforms/platform-registry.js +21 -0
  68. package/dist/platforms/vega/build.d.ts +23 -0
  69. package/dist/platforms/vega/build.d.ts.map +1 -0
  70. package/dist/platforms/vega/build.js +55 -0
  71. package/dist/platforms/vega/device.d.ts +57 -0
  72. package/dist/platforms/vega/device.d.ts.map +1 -0
  73. package/dist/platforms/vega/device.js +206 -0
  74. package/dist/platforms/vega/index.d.ts +4 -0
  75. package/dist/platforms/vega/index.d.ts.map +1 -0
  76. package/dist/platforms/vega/index.js +75 -0
  77. package/dist/platforms/web/index.d.ts +4 -0
  78. package/dist/platforms/web/index.d.ts.map +1 -0
  79. package/dist/platforms/web/index.js +9 -0
  80. package/dist/process.d.ts +3 -0
  81. package/dist/process.d.ts.map +1 -0
  82. package/dist/process.js +28 -0
  83. package/dist/reporters/default-reporter.d.ts +3 -0
  84. package/dist/reporters/default-reporter.d.ts.map +1 -0
  85. package/dist/reporters/default-reporter.js +116 -0
  86. package/dist/reporters/junit-reporter.d.ts +3 -0
  87. package/dist/reporters/junit-reporter.d.ts.map +1 -0
  88. package/dist/reporters/junit-reporter.js +119 -0
  89. package/dist/reporters/live-reporter.d.ts +20 -0
  90. package/dist/reporters/live-reporter.d.ts.map +1 -0
  91. package/dist/reporters/live-reporter.js +176 -0
  92. package/dist/src/reporters/default-reporter.js +135 -0
  93. package/dist/test-reporter-demo.js +95 -0
  94. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  95. package/dist/utils/status-formatter.d.ts +27 -0
  96. package/dist/utils/status-formatter.d.ts.map +1 -0
  97. package/dist/utils/status-formatter.js +54 -0
  98. package/dist/utils.d.ts +6 -0
  99. package/dist/utils.d.ts.map +1 -0
  100. package/dist/utils.js +26 -0
  101. package/dist/wizard/bundleId.js +1 -1
  102. package/dist/wizard/jestIntegration.d.ts +2 -0
  103. package/dist/wizard/jestIntegration.d.ts.map +1 -0
  104. package/dist/wizard/jestIntegration.js +15 -0
  105. package/dist/wizard/packageManager.d.ts +2 -0
  106. package/dist/wizard/packageManager.d.ts.map +1 -0
  107. package/dist/wizard/packageManager.js +10 -0
  108. package/eslint.config.mjs +1 -1
  109. package/package.json +8 -8
  110. package/skills/core.md +92 -0
  111. package/skills/mocking.md +87 -0
  112. package/skills/ui.md +59 -0
  113. package/src/__tests__/platform-commands.test.ts +232 -0
  114. package/src/index.ts +152 -5
  115. package/src/platform-commands.ts +148 -0
  116. package/src/wizard/bundleId.ts +1 -1
@@ -0,0 +1,232 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import type { Config } from '@react-native-harness/config';
3
+ import {
4
+ discoverPlatformCommands,
5
+ runPlatformCommand,
6
+ } from '../platform-commands.js';
7
+
8
+ const createCommandModuleUrl = (body: string) =>
9
+ `data:text/javascript,${encodeURIComponent(body)}`;
10
+
11
+ const globalState = globalThis as typeof globalThis & {
12
+ __platformCommandCall?: unknown;
13
+ };
14
+
15
+ describe('platform CLI command discovery', () => {
16
+ afterEach(() => {
17
+ delete globalState.__platformCommandCall;
18
+ });
19
+
20
+ it('runs a discovered platform command', async () => {
21
+ const moduleUrl = createCommandModuleUrl(`
22
+ export const commands = [{
23
+ name: 'xctest',
24
+ async run(args, context) {
25
+ globalThis.__platformCommandCall = { args, context };
26
+ }
27
+ }];
28
+ `);
29
+ const loadConfig = vi.fn(async () => ({
30
+ projectRoot: '/tmp/project',
31
+ config: {
32
+ entryPoint: 'index.js',
33
+ appRegistryComponentName: 'App',
34
+ runners: [
35
+ {
36
+ name: 'ios',
37
+ config: {},
38
+ runner: '/virtual/runner.js',
39
+ cli: moduleUrl,
40
+ platformId: 'ios',
41
+ },
42
+ ],
43
+ plugins: [],
44
+ metroPort: 8081,
45
+ webSocketPort: undefined,
46
+ bridgeTimeout: 60000,
47
+ platformReadyTimeout: 300000,
48
+ bundleStartTimeout: 60000,
49
+ maxAppRestarts: 2,
50
+ resetEnvironmentBetweenTestFiles: true,
51
+ unstable__skipAlreadyIncludedModules: false,
52
+ unstable__enableMetroCache: false,
53
+ permissions: false,
54
+ detectNativeCrashes: true,
55
+ crashDetectionInterval: 500,
56
+ disableViewFlattening: false,
57
+ forwardClientLogs: false,
58
+ } satisfies Config,
59
+ }));
60
+
61
+ expect(
62
+ await runPlatformCommand({
63
+ argv: ['xctest', 'build', '--destination', 'simulator'],
64
+ cwd: '/tmp/project',
65
+ loadConfig,
66
+ })
67
+ ).toBe(true);
68
+ expect(globalState.__platformCommandCall).toEqual({
69
+ args: ['build', '--destination', 'simulator'],
70
+ context: {
71
+ cwd: '/tmp/project',
72
+ projectRoot: '/tmp/project',
73
+ },
74
+ });
75
+ });
76
+
77
+ it('deduplicates platform CLI modules across runners', async () => {
78
+ const moduleUrl = createCommandModuleUrl(`
79
+ export const commands = [{
80
+ name: 'xctest',
81
+ async run() {}
82
+ }];
83
+ `);
84
+ const loadConfig = vi.fn(async () => ({
85
+ projectRoot: '/tmp/project',
86
+ config: {
87
+ entryPoint: 'index.js',
88
+ appRegistryComponentName: 'App',
89
+ runners: [
90
+ {
91
+ name: 'ios-sim',
92
+ config: {},
93
+ runner: '/virtual/ios-sim-runner.js',
94
+ cli: moduleUrl,
95
+ platformId: 'ios',
96
+ },
97
+ {
98
+ name: 'ios-device',
99
+ config: {},
100
+ runner: '/virtual/ios-device-runner.js',
101
+ cli: moduleUrl,
102
+ platformId: 'ios',
103
+ },
104
+ ],
105
+ plugins: [],
106
+ metroPort: 8081,
107
+ webSocketPort: undefined,
108
+ bridgeTimeout: 60000,
109
+ platformReadyTimeout: 300000,
110
+ bundleStartTimeout: 60000,
111
+ maxAppRestarts: 2,
112
+ resetEnvironmentBetweenTestFiles: true,
113
+ unstable__skipAlreadyIncludedModules: false,
114
+ unstable__enableMetroCache: false,
115
+ permissions: false,
116
+ detectNativeCrashes: true,
117
+ crashDetectionInterval: 500,
118
+ disableViewFlattening: false,
119
+ forwardClientLogs: false,
120
+ } satisfies Config,
121
+ }));
122
+
123
+ const discoveredCommands = await discoverPlatformCommands({
124
+ cwd: '/tmp/project',
125
+ loadConfig,
126
+ });
127
+
128
+ expect(discoveredCommands?.commands).toHaveLength(1);
129
+ });
130
+
131
+ it('returns false when no platform command matches', async () => {
132
+ const loadConfig = vi.fn(async () => ({
133
+ projectRoot: '/tmp/project',
134
+ config: {
135
+ entryPoint: 'index.js',
136
+ appRegistryComponentName: 'App',
137
+ runners: [
138
+ {
139
+ name: 'android',
140
+ config: {},
141
+ runner: '/virtual/android-runner.js',
142
+ platformId: 'android',
143
+ },
144
+ ],
145
+ plugins: [],
146
+ metroPort: 8081,
147
+ webSocketPort: undefined,
148
+ bridgeTimeout: 60000,
149
+ platformReadyTimeout: 300000,
150
+ bundleStartTimeout: 60000,
151
+ maxAppRestarts: 2,
152
+ resetEnvironmentBetweenTestFiles: true,
153
+ unstable__skipAlreadyIncludedModules: false,
154
+ unstable__enableMetroCache: false,
155
+ permissions: false,
156
+ detectNativeCrashes: true,
157
+ crashDetectionInterval: 500,
158
+ disableViewFlattening: false,
159
+ forwardClientLogs: false,
160
+ } satisfies Config,
161
+ }));
162
+
163
+ await expect(
164
+ runPlatformCommand({
165
+ argv: ['xctest', 'build'],
166
+ cwd: '/tmp/project',
167
+ loadConfig,
168
+ })
169
+ ).resolves.toBe(false);
170
+ });
171
+
172
+ it('throws when two platform modules define the same command', async () => {
173
+ const firstModuleUrl = createCommandModuleUrl(`
174
+ export const commands = [{
175
+ name: 'xctest',
176
+ async run() {}
177
+ }];
178
+ `);
179
+ const secondModuleUrl = createCommandModuleUrl(`
180
+ // second module
181
+ export const commands = [{
182
+ name: 'xctest',
183
+ async run() {}
184
+ }];
185
+ `);
186
+ const loadConfig = vi.fn(async () => ({
187
+ projectRoot: '/tmp/project',
188
+ config: {
189
+ entryPoint: 'index.js',
190
+ appRegistryComponentName: 'App',
191
+ runners: [
192
+ {
193
+ name: 'ios',
194
+ config: {},
195
+ runner: '/virtual/ios-runner.js',
196
+ cli: firstModuleUrl,
197
+ platformId: 'ios',
198
+ },
199
+ {
200
+ name: 'android',
201
+ config: {},
202
+ runner: '/virtual/android-runner.js',
203
+ cli: secondModuleUrl,
204
+ platformId: 'android',
205
+ },
206
+ ],
207
+ plugins: [],
208
+ metroPort: 8081,
209
+ webSocketPort: undefined,
210
+ bridgeTimeout: 60000,
211
+ platformReadyTimeout: 300000,
212
+ bundleStartTimeout: 60000,
213
+ maxAppRestarts: 2,
214
+ resetEnvironmentBetweenTestFiles: true,
215
+ unstable__skipAlreadyIncludedModules: false,
216
+ unstable__enableMetroCache: false,
217
+ permissions: false,
218
+ detectNativeCrashes: true,
219
+ crashDetectionInterval: 500,
220
+ disableViewFlattening: false,
221
+ forwardClientLogs: false,
222
+ } satisfies Config,
223
+ }));
224
+
225
+ await expect(
226
+ discoverPlatformCommands({
227
+ cwd: '/tmp/project',
228
+ loadConfig,
229
+ })
230
+ ).rejects.toThrow("Duplicate platform CLI command 'xctest'");
231
+ });
232
+ });
package/src/index.ts CHANGED
@@ -1,11 +1,135 @@
1
1
  import { run, yargsOptions } from 'jest-cli';
2
2
  import { getConfig } from '@react-native-harness/config';
3
3
  import { runInitWizard } from './wizard/index.js';
4
+ import { runPlatformCommand } from './platform-commands.js';
4
5
  import fs from 'node:fs';
5
6
  import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
6
8
 
7
9
  const JEST_CONFIG_EXTENSIONS = ['.mjs', '.js', '.cjs'];
8
10
  const JEST_HARNESS_CONFIG_BASE = 'jest.harness.config';
11
+ const SKILLS_DIRECTORY = path.resolve(
12
+ path.dirname(fileURLToPath(import.meta.url)),
13
+ '../skills'
14
+ );
15
+
16
+ type SkillMetadata = {
17
+ fileName: string;
18
+ name: string;
19
+ description: string;
20
+ };
21
+
22
+ const readSkillMetadata = (fileName: string): SkillMetadata => {
23
+ const filePath = path.join(SKILLS_DIRECTORY, fileName);
24
+ const content = fs.readFileSync(filePath, 'utf8');
25
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
26
+
27
+ const metadata = {
28
+ name: fileName.replace(/\.md$/, ''),
29
+ description: '',
30
+ };
31
+
32
+ if (frontmatterMatch) {
33
+ for (const line of frontmatterMatch[1].split('\n')) {
34
+ const separatorIndex = line.indexOf(':');
35
+
36
+ if (separatorIndex === -1) {
37
+ continue;
38
+ }
39
+
40
+ const key = line.slice(0, separatorIndex).trim();
41
+ const value = line.slice(separatorIndex + 1).trim().replace(/^['"]|['"]$/g, '');
42
+
43
+ if (key === 'name') {
44
+ metadata.name = value;
45
+ }
46
+
47
+ if (key === 'description') {
48
+ metadata.description = value;
49
+ }
50
+ }
51
+ }
52
+
53
+ return {
54
+ fileName,
55
+ name: metadata.name,
56
+ description: metadata.description,
57
+ };
58
+ };
59
+
60
+ const listSkills = () =>
61
+ fs
62
+ .readdirSync(SKILLS_DIRECTORY)
63
+ .filter((file) => file.endsWith('.md'))
64
+ .map(readSkillMetadata)
65
+ .sort((left, right) => left.name.localeCompare(right.name));
66
+
67
+ const printSkillList = () => {
68
+ for (const skill of listSkills()) {
69
+ console.log(`${skill.name}: ${skill.description}`);
70
+ }
71
+ };
72
+
73
+ const printSkillUsage = () => {
74
+ console.log(`Usage: harness skill <command>
75
+
76
+ Commands:
77
+ list List bundled skills
78
+ get <name> Print a bundled skill file
79
+
80
+ Examples:
81
+ harness skill list
82
+ harness skill get core`);
83
+ };
84
+
85
+ const getErrorMessage = (error: unknown): string => {
86
+ if (error instanceof Error) {
87
+ return error.message;
88
+ }
89
+
90
+ return String(error);
91
+ };
92
+
93
+ const runSkillCommand = () => {
94
+ const [, , commandName, subcommand, skillName] = process.argv;
95
+
96
+ if (subcommand === undefined || subcommand === 'list') {
97
+ printSkillList();
98
+ return;
99
+ }
100
+
101
+ if (subcommand === '--help' || subcommand === '-h') {
102
+ printSkillUsage();
103
+ return;
104
+ }
105
+
106
+ if (subcommand === 'get') {
107
+ if (!skillName) {
108
+ console.error('Missing skill name.');
109
+ printSkillUsage();
110
+ process.exit(1);
111
+ }
112
+
113
+ const skillPath = path.join(SKILLS_DIRECTORY, `${skillName}.md`);
114
+
115
+ if (!fs.existsSync(skillPath)) {
116
+ console.error(`Unknown skill '${skillName}'.`);
117
+ console.error(
118
+ `Available skills: ${listSkills()
119
+ .map((skill) => skill.name)
120
+ .join(', ')}`
121
+ );
122
+ process.exit(1);
123
+ }
124
+
125
+ console.log(fs.readFileSync(skillPath, 'utf8'));
126
+ return;
127
+ }
128
+
129
+ console.error(`Unknown ${commandName} subcommand '${subcommand}'.`);
130
+ printSkillUsage();
131
+ process.exit(1);
132
+ };
9
133
 
10
134
  const checkForOldConfig = async () => {
11
135
  try {
@@ -73,9 +197,26 @@ const patchYargsOptions = () => {
73
197
  delete yargsOptions.logHeapUsage;
74
198
  };
75
199
 
76
- if (process.argv.includes('init')) {
77
- runInitWizard();
78
- } else {
200
+ const main = async () => {
201
+ if (process.argv[2] === 'skill' || process.argv[2] === 'skills') {
202
+ runSkillCommand();
203
+ return;
204
+ }
205
+
206
+ if (process.argv.includes('init')) {
207
+ runInitWizard();
208
+ return;
209
+ }
210
+
211
+ if (
212
+ await runPlatformCommand({
213
+ argv: process.argv.slice(2),
214
+ cwd: process.cwd(),
215
+ })
216
+ ) {
217
+ return;
218
+ }
219
+
79
220
  patchYargsOptions();
80
221
 
81
222
  const hasConfigArg =
@@ -96,5 +237,11 @@ if (process.argv.includes('init')) {
96
237
  }
97
238
  }
98
239
 
99
- checkForOldConfig().then(() => run());
100
- }
240
+ await checkForOldConfig();
241
+ run();
242
+ };
243
+
244
+ main().catch((error) => {
245
+ console.error(getErrorMessage(error));
246
+ process.exit(1);
247
+ });
@@ -0,0 +1,148 @@
1
+ import { ConfigNotFoundError, getConfig } from '@react-native-harness/config';
2
+ import type { HarnessCliCommand } from '@react-native-harness/platforms';
3
+
4
+ type ConfigLoader = typeof getConfig;
5
+
6
+ type DiscoveredPlatformCommands = {
7
+ commands: HarnessCliCommand[];
8
+ projectRoot: string;
9
+ };
10
+
11
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
12
+ typeof value === 'object' && value !== null;
13
+
14
+ const getModuleCommands = (
15
+ importedModule: unknown,
16
+ modulePath: string
17
+ ): HarnessCliCommand[] => {
18
+ const moduleValue = isRecord(importedModule)
19
+ ? importedModule
20
+ : ({} as Record<string, unknown>);
21
+ const defaultExport = isRecord(moduleValue.default)
22
+ ? moduleValue.default
23
+ : undefined;
24
+ const commandsValue = moduleValue.commands ?? defaultExport?.commands;
25
+
26
+ if (!Array.isArray(commandsValue)) {
27
+ throw new Error(
28
+ `Invalid platform CLI module '${modulePath}': expected a commands array.`
29
+ );
30
+ }
31
+
32
+ return commandsValue.map((command, index) => {
33
+ if (!isRecord(command) || typeof command.name !== 'string') {
34
+ throw new Error(
35
+ `Invalid platform CLI module '${modulePath}': command #${index + 1} is missing a valid name.`
36
+ );
37
+ }
38
+
39
+ if (typeof command.run !== 'function') {
40
+ throw new Error(
41
+ `Invalid platform CLI module '${modulePath}': command '${command.name}' is missing a run handler.`
42
+ );
43
+ }
44
+
45
+ if (
46
+ command.aliases !== undefined &&
47
+ (!Array.isArray(command.aliases) ||
48
+ command.aliases.some((alias) => typeof alias !== 'string'))
49
+ ) {
50
+ throw new Error(
51
+ `Invalid platform CLI module '${modulePath}': command '${command.name}' has invalid aliases.`
52
+ );
53
+ }
54
+
55
+ return command as HarnessCliCommand;
56
+ });
57
+ };
58
+
59
+ const registerCommandNames = (
60
+ seenNames: Map<string, string>,
61
+ modulePath: string,
62
+ command: HarnessCliCommand
63
+ ) => {
64
+ const names = [command.name, ...(command.aliases ?? [])];
65
+
66
+ for (const name of names) {
67
+ const existingSource = seenNames.get(name);
68
+
69
+ if (existingSource !== undefined) {
70
+ throw new Error(
71
+ `Duplicate platform CLI command '${name}' in '${modulePath}' and '${existingSource}'.`
72
+ );
73
+ }
74
+
75
+ seenNames.set(name, modulePath);
76
+ }
77
+ };
78
+
79
+ export const discoverPlatformCommands = async (options: {
80
+ cwd: string;
81
+ loadConfig?: ConfigLoader;
82
+ }): Promise<DiscoveredPlatformCommands | null> => {
83
+ const loadConfig = options.loadConfig ?? getConfig;
84
+
85
+ try {
86
+ const { config, projectRoot } = await loadConfig(options.cwd);
87
+ const modulePaths = [...new Set(config.runners.map((runner) => runner.cli))].filter(
88
+ (modulePath): modulePath is string => typeof modulePath === 'string'
89
+ );
90
+ const commands: HarnessCliCommand[] = [];
91
+ const seenNames = new Map<string, string>();
92
+
93
+ for (const modulePath of modulePaths) {
94
+ const importedModule = await import(modulePath);
95
+ const moduleCommands = getModuleCommands(importedModule, modulePath);
96
+
97
+ for (const command of moduleCommands) {
98
+ registerCommandNames(seenNames, modulePath, command);
99
+ commands.push(command);
100
+ }
101
+ }
102
+
103
+ return {
104
+ commands,
105
+ projectRoot,
106
+ };
107
+ } catch (error) {
108
+ if (error instanceof ConfigNotFoundError) {
109
+ return null;
110
+ }
111
+
112
+ throw error;
113
+ }
114
+ };
115
+
116
+ export const runPlatformCommand = async (options: {
117
+ argv: string[];
118
+ cwd: string;
119
+ loadConfig?: ConfigLoader;
120
+ }): Promise<boolean> => {
121
+ const commandName = options.argv[0];
122
+
123
+ if (typeof commandName !== 'string' || commandName.length === 0) {
124
+ return false;
125
+ }
126
+
127
+ const discoveredCommands = await discoverPlatformCommands(options);
128
+
129
+ if (discoveredCommands === null) {
130
+ return false;
131
+ }
132
+
133
+ const command = discoveredCommands.commands.find(
134
+ (entry) =>
135
+ entry.name === commandName || entry.aliases?.includes(commandName) === true
136
+ );
137
+
138
+ if (command === undefined) {
139
+ return false;
140
+ }
141
+
142
+ await command.run(options.argv.slice(1), {
143
+ cwd: options.cwd,
144
+ projectRoot: discoveredCommands.projectRoot,
145
+ });
146
+
147
+ return true;
148
+ };
@@ -59,7 +59,7 @@ export const getBundleIds = async (
59
59
  try {
60
60
  new URL(value);
61
61
  return;
62
- } catch (_e) {
62
+ } catch {
63
63
  return 'Please enter a valid URL';
64
64
  }
65
65
  },