@react-native-harness/cli 1.0.0-alpha.23 → 1.0.0-alpha.25

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 (41) hide show
  1. package/dist/index.js +21 -4
  2. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  3. package/dist/wizard/bundleId.d.ts +2 -0
  4. package/dist/wizard/bundleId.d.ts.map +1 -0
  5. package/dist/wizard/bundleId.js +68 -0
  6. package/dist/wizard/configGenerator.d.ts +4 -0
  7. package/dist/wizard/configGenerator.d.ts.map +1 -0
  8. package/dist/wizard/configGenerator.js +89 -0
  9. package/dist/wizard/index.d.ts +2 -0
  10. package/dist/wizard/index.d.ts.map +1 -0
  11. package/dist/wizard/index.js +40 -0
  12. package/dist/wizard/jestConfig.d.ts +2 -0
  13. package/dist/wizard/jestConfig.d.ts.map +1 -0
  14. package/dist/wizard/jestConfig.js +22 -0
  15. package/dist/wizard/jestIntegration.d.ts +2 -0
  16. package/dist/wizard/jestIntegration.d.ts.map +1 -0
  17. package/dist/wizard/jestIntegration.js +15 -0
  18. package/dist/wizard/packageManager.d.ts +2 -0
  19. package/dist/wizard/packageManager.d.ts.map +1 -0
  20. package/dist/wizard/packageManager.js +10 -0
  21. package/dist/wizard/platforms.d.ts +2 -0
  22. package/dist/wizard/platforms.d.ts.map +1 -0
  23. package/dist/wizard/platforms.js +34 -0
  24. package/dist/wizard/projectType.d.ts +6 -0
  25. package/dist/wizard/projectType.d.ts.map +1 -0
  26. package/dist/wizard/projectType.js +63 -0
  27. package/dist/wizard/targets.d.ts +3 -0
  28. package/dist/wizard/targets.d.ts.map +1 -0
  29. package/dist/wizard/targets.js +62 -0
  30. package/eslint.config.mjs +1 -1
  31. package/package.json +9 -4
  32. package/src/index.ts +33 -4
  33. package/src/wizard/bundleId.ts +70 -0
  34. package/src/wizard/configGenerator.ts +139 -0
  35. package/src/wizard/index.ts +72 -0
  36. package/src/wizard/jestConfig.ts +28 -0
  37. package/src/wizard/platforms.ts +44 -0
  38. package/src/wizard/projectType.ts +77 -0
  39. package/src/wizard/targets.ts +78 -0
  40. package/tsconfig.json +15 -0
  41. package/tsconfig.lib.json +15 -0
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { RunTarget } from '@react-native-harness/platforms';
4
+ import type { ProjectConfig } from './projectType.js';
5
+
6
+ const q = (s: string) => `'${s.replace(/'/g, "\\'")}'`;
7
+
8
+ const getDeviceCall = (target: RunTarget): string => {
9
+ const { device, type, platform } = target;
10
+ if (platform === 'android') {
11
+ if (type === 'emulator') {
12
+ return `androidEmulator(${q(device.name)})`;
13
+ }
14
+ return `physicalAndroidDevice(${q(device.manufacturer)}, ${q(
15
+ device.model
16
+ )})`;
17
+ }
18
+ if (platform === 'ios') {
19
+ if (type === 'emulator') {
20
+ return `appleSimulator(${q(device.name)}, ${q(device.systemVersion)})`;
21
+ }
22
+ return `applePhysicalDevice(${q(device.name)})`;
23
+ }
24
+ return JSON.stringify(device);
25
+ };
26
+
27
+ const getPlatformFn = (platform: string): string => {
28
+ if (platform === 'android') return 'androidPlatform';
29
+ if (platform === 'ios') return 'applePlatform';
30
+ return `${platform}Platform`;
31
+ };
32
+
33
+ export const generateConfig = (
34
+ projectConfig: ProjectConfig,
35
+ selectedPlatforms: string[],
36
+ selectedTargets: RunTarget[],
37
+ bundleIds: Record<string, string>
38
+ ) => {
39
+ const imports: string[] = [];
40
+ if (selectedPlatforms.includes('android')) {
41
+ const androidFactories = ['androidPlatform'];
42
+ if (
43
+ selectedTargets.some(
44
+ (t) => t.platform === 'android' && t.type === 'emulator'
45
+ )
46
+ )
47
+ androidFactories.push('androidEmulator');
48
+ if (
49
+ selectedTargets.some(
50
+ (t) => t.platform === 'android' && t.type === 'physical'
51
+ )
52
+ )
53
+ androidFactories.push('physicalAndroidDevice');
54
+
55
+ imports.push(
56
+ `import { ${androidFactories.join(
57
+ ', '
58
+ )} } from "@react-native-harness/platform-android";`
59
+ );
60
+ }
61
+ if (selectedPlatforms.includes('ios')) {
62
+ const iosFactories = ['applePlatform'];
63
+ if (
64
+ selectedTargets.some((t) => t.platform === 'ios' && t.type === 'emulator')
65
+ )
66
+ iosFactories.push('appleSimulator');
67
+ if (
68
+ selectedTargets.some((t) => t.platform === 'ios' && t.type === 'physical')
69
+ )
70
+ iosFactories.push('applePhysicalDevice');
71
+
72
+ imports.push(
73
+ `import { ${iosFactories.join(
74
+ ', '
75
+ )} } from "@react-native-harness/platform-apple";`
76
+ );
77
+ }
78
+ if (selectedPlatforms.includes('web')) {
79
+ const webFactories = ['webPlatform'];
80
+ const browsers = new Set(
81
+ selectedTargets
82
+ .filter((t) => t.platform === 'web')
83
+ .map((t) => t.device.browserType)
84
+ );
85
+ for (const browser of browsers) {
86
+ if (browser) webFactories.push(browser);
87
+ }
88
+
89
+ imports.push(
90
+ `import { ${webFactories.join(
91
+ ', '
92
+ )} } from "@react-native-harness/platform-web";`
93
+ );
94
+ }
95
+
96
+ const runnerConfigs = selectedTargets.map((target) => {
97
+ const platformFn = getPlatformFn(target.platform);
98
+ const name = target.name.toLowerCase().replace(/\s+/g, '-');
99
+
100
+ if (target.platform === 'web') {
101
+ const url = bundleIds[target.platform];
102
+ const browserCall = `${target.device.browserType}(${q(url)})`;
103
+ return ` ${platformFn}({
104
+ name: ${q(name)},
105
+ browser: ${browserCall},
106
+ }),`;
107
+ }
108
+
109
+ const bundleId = bundleIds[target.platform];
110
+ const deviceCall = getDeviceCall(target);
111
+
112
+ return ` ${platformFn}({
113
+ name: ${q(name)},
114
+ device: ${deviceCall},
115
+ bundleId: ${q(bundleId)},
116
+ }),`;
117
+ });
118
+
119
+ const configContent = `
120
+ ${imports.join('\n')}
121
+
122
+ export default {
123
+ entryPoint: ${q(projectConfig.entryPoint)},
124
+ appRegistryComponentName: ${q(projectConfig.appRegistryComponentName)},
125
+
126
+ runners: [
127
+ ${runnerConfigs.join('\n')}
128
+ ],
129
+ defaultRunner: ${q(
130
+ selectedTargets[0].name.toLowerCase().replace(/\s+/g, '-')
131
+ )},
132
+ };
133
+ `;
134
+
135
+ fs.writeFileSync(
136
+ path.join(process.cwd(), 'rn-harness.config.mjs'),
137
+ configContent.trim() + '\n'
138
+ );
139
+ };
@@ -0,0 +1,72 @@
1
+ import {
2
+ intro,
3
+ outro,
4
+ note,
5
+ isProject,
6
+ cancelPromptAndExit,
7
+ promptConfirm,
8
+ } from '@react-native-harness/tools';
9
+ import { getProjectConfig } from './projectType.js';
10
+ import { installPlatforms } from './platforms.js';
11
+ import { discoverTargets } from './targets.js';
12
+ import { getBundleIds } from './bundleId.js';
13
+ import { generateConfig } from './configGenerator.js';
14
+ import { createJestConfig } from './jestConfig.js';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ const CONFIG_EXTENSIONS = ['.js', '.mjs', '.cjs', '.json'];
19
+
20
+ const checkForExistingConfig = async (projectRoot: string) => {
21
+ const existingConfig = CONFIG_EXTENSIONS.find((ext) =>
22
+ fs.existsSync(path.join(projectRoot, `rn-harness.config${ext}`))
23
+ );
24
+
25
+ if (existingConfig) {
26
+ const shouldOverwrite = await promptConfirm({
27
+ message: `A configuration file (rn-harness.config${existingConfig}) already exists. Are you sure you want to overwrite it?`,
28
+ confirmLabel: 'Overwrite',
29
+ cancelLabel: 'Keep existing',
30
+ });
31
+
32
+ if (!shouldOverwrite) {
33
+ cancelPromptAndExit('Setup cancelled. Keeping existing configuration.');
34
+ }
35
+ }
36
+ };
37
+
38
+ export const runInitWizard = async () => {
39
+ const projectRoot = process.cwd();
40
+
41
+ if (!isProject(projectRoot)) {
42
+ cancelPromptAndExit(
43
+ 'React Native Harness must be run in a React Native project root (directory with package.json containing react-native).'
44
+ );
45
+ }
46
+
47
+ intro('React Native Harness');
48
+
49
+ note(
50
+ "This wizard will guide you through the setup process to get React Native Harness up and running in your project. We'll help you configure your project type, platforms, and test targets.",
51
+ 'Configuration wizard'
52
+ );
53
+
54
+ await checkForExistingConfig(projectRoot);
55
+
56
+ const projectConfig = await getProjectConfig();
57
+ const selectedPlatforms = await installPlatforms();
58
+ const selectedTargets = await discoverTargets(selectedPlatforms);
59
+ const bundleIds = await getBundleIds(selectedPlatforms);
60
+
61
+ generateConfig(projectConfig, selectedPlatforms, selectedTargets, bundleIds);
62
+ await createJestConfig(projectRoot);
63
+
64
+ note(
65
+ 'A dedicated Jest configuration (jest.harness.config.mjs) has been created. Harness will automatically use it when you run the "harness" command.',
66
+ 'Jest configuration'
67
+ );
68
+
69
+ outro(
70
+ 'Setup complete. Happy testing!'
71
+ );
72
+ };
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { promptConfirm } from '@react-native-harness/tools';
4
+
5
+ export const createJestConfig = async (projectRoot: string) => {
6
+ const configPath = path.join(projectRoot, 'jest.harness.config.mjs');
7
+
8
+ if (fs.existsSync(configPath)) {
9
+ const shouldOverwrite = await promptConfirm({
10
+ message:
11
+ 'A dedicated Jest configuration (jest.harness.config.mjs) already exists. Do you want to overwrite it?',
12
+ confirmLabel: 'Overwrite',
13
+ cancelLabel: 'Keep existing',
14
+ });
15
+
16
+ if (!shouldOverwrite) {
17
+ return;
18
+ }
19
+ }
20
+
21
+ const configContent = `export default {
22
+ preset: 'react-native-harness',
23
+ testMatch: ['<rootDir>/**/__tests__/**/*.harness.[jt]s?(x)'],
24
+ };
25
+ `;
26
+
27
+ fs.writeFileSync(configPath, configContent);
28
+ };
@@ -0,0 +1,44 @@
1
+ import {
2
+ promptMultiselect,
3
+ spinner,
4
+ cancelPromptAndExit,
5
+ installDevDependency,
6
+ } from '@react-native-harness/tools';
7
+
8
+ export const installPlatforms = async (): Promise<string[]> => {
9
+ const projectRoot = process.cwd();
10
+ const selectedPlatforms = await promptMultiselect({
11
+ message: 'Select platforms to support',
12
+ options: [
13
+ { value: 'android', label: 'Android' },
14
+ { value: 'ios', label: 'iOS' },
15
+ { value: 'web', label: 'Web' },
16
+ ],
17
+ });
18
+
19
+ if (selectedPlatforms.length === 0) {
20
+ cancelPromptAndExit('At least one platform must be selected.');
21
+ }
22
+
23
+ const installSpinner = spinner();
24
+ installSpinner.start('Installing platform packages...');
25
+
26
+ const packagesToInstall: string[] = [];
27
+ if (selectedPlatforms.includes('android'))
28
+ packagesToInstall.push('@react-native-harness/platform-android');
29
+ if (selectedPlatforms.includes('ios'))
30
+ packagesToInstall.push('@react-native-harness/platform-apple');
31
+ if (selectedPlatforms.includes('web'))
32
+ packagesToInstall.push('@react-native-harness/platform-web');
33
+
34
+ try {
35
+ await installDevDependency(projectRoot, packagesToInstall);
36
+ installSpinner.stop('Platform packages installed successfully.');
37
+ } catch (error) {
38
+ installSpinner.stop('Failed to install platform packages.', 1);
39
+ console.error(error);
40
+ process.exit(1);
41
+ }
42
+
43
+ return selectedPlatforms as string[];
44
+ };
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { promptSelect, promptText } from '@react-native-harness/tools';
4
+
5
+ export type ProjectConfig = {
6
+ entryPoint: string;
7
+ appRegistryComponentName: string;
8
+ };
9
+
10
+ const tryExtractComponentName = (entryPoint: string): string | undefined => {
11
+ try {
12
+ const filePath = path.resolve(process.cwd(), entryPoint);
13
+ if (!fs.existsSync(filePath)) return undefined;
14
+
15
+ const content = fs.readFileSync(filePath, 'utf8');
16
+ // Look for AppRegistry.registerComponent('Name', ...) or AppRegistry.registerComponent("Name", ...)
17
+ const match = content.match(
18
+ /AppRegistry\.registerComponent\(\s*['"](.+?)['"]/
19
+ );
20
+
21
+ return match ? match[1] : undefined;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ };
26
+
27
+ export const getProjectConfig = async (): Promise<ProjectConfig> => {
28
+ const projectType = await promptSelect({
29
+ message: 'What type of project is this?',
30
+ options: [
31
+ { value: 'expo', label: 'Expo' },
32
+ { value: 'custom', label: 'React Native CLI / Custom' },
33
+ ],
34
+ });
35
+
36
+ if (projectType === 'expo') {
37
+ try {
38
+ const packageJson = JSON.parse(
39
+ fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')
40
+ );
41
+ return {
42
+ entryPoint: packageJson.main || './node_modules/expo/AppEntry.js',
43
+ appRegistryComponentName: 'main',
44
+ };
45
+ } catch {
46
+ return {
47
+ entryPoint: './node_modules/expo/AppEntry.js',
48
+ appRegistryComponentName: 'main',
49
+ };
50
+ }
51
+ }
52
+
53
+ const entryPoint = await promptText({
54
+ message: 'Enter the path to your app entry file',
55
+ initialValue: './index.js',
56
+ placeholder: './index.js',
57
+ validate: (value: string | undefined) => {
58
+ if (!value) return 'Entry point is required';
59
+ return;
60
+ },
61
+ });
62
+
63
+ const suggestedComponentName = tryExtractComponentName(entryPoint);
64
+
65
+ const appRegistryComponentName = await promptText({
66
+ message:
67
+ 'Enter the name of the component registered via AppRegistry.registerComponent()',
68
+ placeholder: 'MyAppName',
69
+ initialValue: suggestedComponentName,
70
+ validate: (value: string | undefined) => {
71
+ if (!value) return 'Component name is required';
72
+ return;
73
+ },
74
+ });
75
+
76
+ return { entryPoint, appRegistryComponentName };
77
+ };
@@ -0,0 +1,78 @@
1
+ import {
2
+ spinner,
3
+ promptAutocompleteMultiselect,
4
+ cancelPromptAndExit,
5
+ } from '@react-native-harness/tools';
6
+ import type { RunTarget } from '@react-native-harness/platforms';
7
+
8
+ export const discoverTargets = async (
9
+ selectedPlatforms: string[]
10
+ ): Promise<RunTarget[]> => {
11
+ const allTargets: RunTarget[] = [];
12
+ const targetSpinner = spinner();
13
+ targetSpinner.start('Discovering available targets...');
14
+
15
+ if (selectedPlatforms.includes('android')) {
16
+ try {
17
+ const androidPlatform = await import(
18
+ '@react-native-harness/platform-android'
19
+ );
20
+ const targets: RunTarget[] = await androidPlatform.getRunTargets();
21
+ allTargets.push(...targets);
22
+ } catch (e) {
23
+ console.error('Failed to load Android targets:', e);
24
+ }
25
+ }
26
+
27
+ if (selectedPlatforms.includes('ios')) {
28
+ try {
29
+ const applePlatform = await import(
30
+ '@react-native-harness/platform-apple'
31
+ );
32
+ const targets: RunTarget[] = await applePlatform.getRunTargets();
33
+ allTargets.push(...targets);
34
+ } catch (e) {
35
+ console.error('Failed to load iOS targets:', e);
36
+ }
37
+ }
38
+
39
+ if (selectedPlatforms.includes('web')) {
40
+ try {
41
+ const webPlatform = await import('@react-native-harness/platform-web');
42
+ const targets: RunTarget[] = await webPlatform.getRunTargets();
43
+ allTargets.push(...targets);
44
+ } catch (e) {
45
+ console.error('Failed to load Web targets:', e);
46
+ }
47
+ }
48
+
49
+ targetSpinner.stop('Target discovery complete.');
50
+
51
+ if (allTargets.length === 0) {
52
+ cancelPromptAndExit('No available targets (emulators or devices) found.');
53
+ }
54
+
55
+ const options = allTargets.map((target, index) => {
56
+ let platformLabel = 'Unknown';
57
+ if (target.platform === 'android') platformLabel = 'Android';
58
+ else if (target.platform === 'ios') platformLabel = 'iOS';
59
+ else if (target.platform === 'web') platformLabel = 'Web';
60
+
61
+ return {
62
+ value: index,
63
+ label: `[${platformLabel}] ${target.name} (${target.type})`,
64
+ hint: target.description,
65
+ };
66
+ });
67
+
68
+ const selectedTargetIndices = await promptAutocompleteMultiselect<number>({
69
+ message: 'Select targets to support in Harness',
70
+ options,
71
+ });
72
+
73
+ if (selectedTargetIndices.length === 0) {
74
+ cancelPromptAndExit('At least one target must be selected.');
75
+ }
76
+
77
+ return selectedTargetIndices.map((i) => allTargets[i]);
78
+ };
package/tsconfig.json CHANGED
@@ -3,12 +3,27 @@
3
3
  "files": [],
4
4
  "include": [],
5
5
  "references": [
6
+ {
7
+ "path": "../platform-web"
8
+ },
9
+ {
10
+ "path": "../tools"
11
+ },
12
+ {
13
+ "path": "../platforms"
14
+ },
6
15
  {
7
16
  "path": "../config"
8
17
  },
9
18
  {
10
19
  "path": "../bridge"
11
20
  },
21
+ {
22
+ "path": "../platform-ios"
23
+ },
24
+ {
25
+ "path": "../platform-android"
26
+ },
12
27
  {
13
28
  "path": "./tsconfig.lib.json"
14
29
  }
package/tsconfig.lib.json CHANGED
@@ -12,11 +12,26 @@
12
12
  },
13
13
  "include": ["src/**/*.ts"],
14
14
  "references": [
15
+ {
16
+ "path": "../platform-web/tsconfig.lib.json"
17
+ },
18
+ {
19
+ "path": "../tools/tsconfig.lib.json"
20
+ },
21
+ {
22
+ "path": "../platforms/tsconfig.lib.json"
23
+ },
15
24
  {
16
25
  "path": "../config/tsconfig.lib.json"
17
26
  },
18
27
  {
19
28
  "path": "../bridge/tsconfig.lib.json"
29
+ },
30
+ {
31
+ "path": "../platform-ios/tsconfig.lib.json"
32
+ },
33
+ {
34
+ "path": "../platform-android/tsconfig.lib.json"
20
35
  }
21
36
  ]
22
37
  }