@react-native-harness/cli 1.0.0-canary.1764675030942 → 1.0.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/dist/index.js +21 -4
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/wizard/bundleId.d.ts +2 -0
- package/dist/wizard/bundleId.d.ts.map +1 -0
- package/dist/wizard/bundleId.js +68 -0
- package/dist/wizard/configGenerator.d.ts +4 -0
- package/dist/wizard/configGenerator.d.ts.map +1 -0
- package/dist/wizard/configGenerator.js +89 -0
- package/dist/wizard/index.d.ts +2 -0
- package/dist/wizard/index.d.ts.map +1 -0
- package/dist/wizard/index.js +40 -0
- package/dist/wizard/jestConfig.d.ts +2 -0
- package/dist/wizard/jestConfig.d.ts.map +1 -0
- package/dist/wizard/jestConfig.js +22 -0
- package/dist/wizard/jestIntegration.d.ts +2 -0
- package/dist/wizard/jestIntegration.d.ts.map +1 -0
- package/dist/wizard/jestIntegration.js +15 -0
- package/dist/wizard/packageManager.d.ts +2 -0
- package/dist/wizard/packageManager.d.ts.map +1 -0
- package/dist/wizard/packageManager.js +10 -0
- package/dist/wizard/platforms.d.ts +2 -0
- package/dist/wizard/platforms.d.ts.map +1 -0
- package/dist/wizard/platforms.js +34 -0
- package/dist/wizard/projectType.d.ts +6 -0
- package/dist/wizard/projectType.d.ts.map +1 -0
- package/dist/wizard/projectType.js +63 -0
- package/dist/wizard/targets.d.ts +3 -0
- package/dist/wizard/targets.d.ts.map +1 -0
- package/dist/wizard/targets.js +62 -0
- package/eslint.config.mjs +1 -1
- package/package.json +9 -4
- package/src/index.ts +33 -4
- package/src/wizard/bundleId.ts +70 -0
- package/src/wizard/configGenerator.ts +139 -0
- package/src/wizard/index.ts +72 -0
- package/src/wizard/jestConfig.ts +28 -0
- package/src/wizard/platforms.ts +44 -0
- package/src/wizard/projectType.ts +77 -0
- package/src/wizard/targets.ts +78 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +15 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { promptText } from '@react-native-harness/tools';
|
|
2
|
+
export const getBundleIds = async (selectedPlatforms) => {
|
|
3
|
+
const bundleIds = {};
|
|
4
|
+
if (selectedPlatforms.includes('android')) {
|
|
5
|
+
bundleIds.android = await promptText({
|
|
6
|
+
message: 'Enter Android package name',
|
|
7
|
+
placeholder: 'com.example.app',
|
|
8
|
+
validate: (value) => {
|
|
9
|
+
if (!value)
|
|
10
|
+
return 'Package name is required';
|
|
11
|
+
const parts = value.split('.');
|
|
12
|
+
if (parts.length < 2) {
|
|
13
|
+
return 'Package name must have at least two segments (e.g., com.example)';
|
|
14
|
+
}
|
|
15
|
+
for (const segment of parts) {
|
|
16
|
+
if (!segment)
|
|
17
|
+
return 'Segments cannot be empty';
|
|
18
|
+
if (!/^[a-zA-Z]/.test(segment)) {
|
|
19
|
+
return `Segment "${segment}" must start with a letter`;
|
|
20
|
+
}
|
|
21
|
+
if (!/^[a-zA-Z0-9_]+$/.test(segment)) {
|
|
22
|
+
return `Segment "${segment}" can only contain alphanumeric characters or underscores`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (selectedPlatforms.includes('ios')) {
|
|
30
|
+
bundleIds.ios = await promptText({
|
|
31
|
+
message: 'Enter iOS bundle identifier',
|
|
32
|
+
placeholder: 'com.example.app',
|
|
33
|
+
validate: (value) => {
|
|
34
|
+
if (!value)
|
|
35
|
+
return 'Bundle identifier is required';
|
|
36
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(value)) {
|
|
37
|
+
return 'Bundle identifier can only contain alphanumeric characters, hyphens, and periods';
|
|
38
|
+
}
|
|
39
|
+
if (value.startsWith('.') || value.endsWith('.')) {
|
|
40
|
+
return 'Bundle identifier cannot start or end with a period';
|
|
41
|
+
}
|
|
42
|
+
if (value.includes('..')) {
|
|
43
|
+
return 'Bundle identifier cannot contain consecutive periods';
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (selectedPlatforms.includes('web')) {
|
|
50
|
+
bundleIds.web = await promptText({
|
|
51
|
+
message: 'Enter application URL',
|
|
52
|
+
initialValue: 'http://localhost:8081/index.html',
|
|
53
|
+
placeholder: 'http://localhost:8081/index.html',
|
|
54
|
+
validate: (value) => {
|
|
55
|
+
if (!value)
|
|
56
|
+
return 'URL is required';
|
|
57
|
+
try {
|
|
58
|
+
new URL(value);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return 'Please enter a valid URL';
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return bundleIds;
|
|
68
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RunTarget } from '@react-native-harness/platforms';
|
|
2
|
+
import type { ProjectConfig } from './projectType.js';
|
|
3
|
+
export declare const generateConfig: (projectConfig: ProjectConfig, selectedPlatforms: string[], selectedTargets: RunTarget[], bundleIds: Record<string, string>) => void;
|
|
4
|
+
//# sourceMappingURL=configGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configGenerator.d.ts","sourceRoot":"","sources":["../../src/wizard/configGenerator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AA6BtD,eAAO,MAAM,cAAc,GACzB,eAAe,aAAa,EAC5B,mBAAmB,MAAM,EAAE,EAC3B,iBAAiB,SAAS,EAAE,EAC5B,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAsGlC,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const q = (s) => `'${s.replace(/'/g, "\\'")}'`;
|
|
4
|
+
const getDeviceCall = (target) => {
|
|
5
|
+
const { device, type, platform } = target;
|
|
6
|
+
if (platform === 'android') {
|
|
7
|
+
if (type === 'emulator') {
|
|
8
|
+
return `androidEmulator(${q(device.name)})`;
|
|
9
|
+
}
|
|
10
|
+
return `physicalAndroidDevice(${q(device.manufacturer)}, ${q(device.model)})`;
|
|
11
|
+
}
|
|
12
|
+
if (platform === 'ios') {
|
|
13
|
+
if (type === 'emulator') {
|
|
14
|
+
return `appleSimulator(${q(device.name)}, ${q(device.systemVersion)})`;
|
|
15
|
+
}
|
|
16
|
+
return `applePhysicalDevice(${q(device.name)})`;
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(device);
|
|
19
|
+
};
|
|
20
|
+
const getPlatformFn = (platform) => {
|
|
21
|
+
if (platform === 'android')
|
|
22
|
+
return 'androidPlatform';
|
|
23
|
+
if (platform === 'ios')
|
|
24
|
+
return 'applePlatform';
|
|
25
|
+
return `${platform}Platform`;
|
|
26
|
+
};
|
|
27
|
+
export const generateConfig = (projectConfig, selectedPlatforms, selectedTargets, bundleIds) => {
|
|
28
|
+
const imports = [];
|
|
29
|
+
if (selectedPlatforms.includes('android')) {
|
|
30
|
+
const androidFactories = ['androidPlatform'];
|
|
31
|
+
if (selectedTargets.some((t) => t.platform === 'android' && t.type === 'emulator'))
|
|
32
|
+
androidFactories.push('androidEmulator');
|
|
33
|
+
if (selectedTargets.some((t) => t.platform === 'android' && t.type === 'physical'))
|
|
34
|
+
androidFactories.push('physicalAndroidDevice');
|
|
35
|
+
imports.push(`import { ${androidFactories.join(', ')} } from "@react-native-harness/platform-android";`);
|
|
36
|
+
}
|
|
37
|
+
if (selectedPlatforms.includes('ios')) {
|
|
38
|
+
const iosFactories = ['applePlatform'];
|
|
39
|
+
if (selectedTargets.some((t) => t.platform === 'ios' && t.type === 'emulator'))
|
|
40
|
+
iosFactories.push('appleSimulator');
|
|
41
|
+
if (selectedTargets.some((t) => t.platform === 'ios' && t.type === 'physical'))
|
|
42
|
+
iosFactories.push('applePhysicalDevice');
|
|
43
|
+
imports.push(`import { ${iosFactories.join(', ')} } from "@react-native-harness/platform-apple";`);
|
|
44
|
+
}
|
|
45
|
+
if (selectedPlatforms.includes('web')) {
|
|
46
|
+
const webFactories = ['webPlatform'];
|
|
47
|
+
const browsers = new Set(selectedTargets
|
|
48
|
+
.filter((t) => t.platform === 'web')
|
|
49
|
+
.map((t) => t.device.browserType));
|
|
50
|
+
for (const browser of browsers) {
|
|
51
|
+
if (browser)
|
|
52
|
+
webFactories.push(browser);
|
|
53
|
+
}
|
|
54
|
+
imports.push(`import { ${webFactories.join(', ')} } from "@react-native-harness/platform-web";`);
|
|
55
|
+
}
|
|
56
|
+
const runnerConfigs = selectedTargets.map((target) => {
|
|
57
|
+
const platformFn = getPlatformFn(target.platform);
|
|
58
|
+
const name = target.name.toLowerCase().replace(/\s+/g, '-');
|
|
59
|
+
if (target.platform === 'web') {
|
|
60
|
+
const url = bundleIds[target.platform];
|
|
61
|
+
const browserCall = `${target.device.browserType}(${q(url)})`;
|
|
62
|
+
return ` ${platformFn}({
|
|
63
|
+
name: ${q(name)},
|
|
64
|
+
browser: ${browserCall},
|
|
65
|
+
}),`;
|
|
66
|
+
}
|
|
67
|
+
const bundleId = bundleIds[target.platform];
|
|
68
|
+
const deviceCall = getDeviceCall(target);
|
|
69
|
+
return ` ${platformFn}({
|
|
70
|
+
name: ${q(name)},
|
|
71
|
+
device: ${deviceCall},
|
|
72
|
+
bundleId: ${q(bundleId)},
|
|
73
|
+
}),`;
|
|
74
|
+
});
|
|
75
|
+
const configContent = `
|
|
76
|
+
${imports.join('\n')}
|
|
77
|
+
|
|
78
|
+
export default {
|
|
79
|
+
entryPoint: ${q(projectConfig.entryPoint)},
|
|
80
|
+
appRegistryComponentName: ${q(projectConfig.appRegistryComponentName)},
|
|
81
|
+
|
|
82
|
+
runners: [
|
|
83
|
+
${runnerConfigs.join('\n')}
|
|
84
|
+
],
|
|
85
|
+
defaultRunner: ${q(selectedTargets[0].name.toLowerCase().replace(/\s+/g, '-'))},
|
|
86
|
+
};
|
|
87
|
+
`;
|
|
88
|
+
fs.writeFileSync(path.join(process.cwd(), 'rn-harness.config.mjs'), configContent.trim() + '\n');
|
|
89
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/wizard/index.ts"],"names":[],"mappings":"AAqCA,eAAO,MAAM,aAAa,qBAkCzB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { intro, outro, note, isProject, cancelPromptAndExit, promptConfirm, } from '@react-native-harness/tools';
|
|
2
|
+
import { getProjectConfig } from './projectType.js';
|
|
3
|
+
import { installPlatforms } from './platforms.js';
|
|
4
|
+
import { discoverTargets } from './targets.js';
|
|
5
|
+
import { getBundleIds } from './bundleId.js';
|
|
6
|
+
import { generateConfig } from './configGenerator.js';
|
|
7
|
+
import { createJestConfig } from './jestConfig.js';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
const CONFIG_EXTENSIONS = ['.js', '.mjs', '.cjs', '.json'];
|
|
11
|
+
const checkForExistingConfig = async (projectRoot) => {
|
|
12
|
+
const existingConfig = CONFIG_EXTENSIONS.find((ext) => fs.existsSync(path.join(projectRoot, `rn-harness.config${ext}`)));
|
|
13
|
+
if (existingConfig) {
|
|
14
|
+
const shouldOverwrite = await promptConfirm({
|
|
15
|
+
message: `A configuration file (rn-harness.config${existingConfig}) already exists. Are you sure you want to overwrite it?`,
|
|
16
|
+
confirmLabel: 'Overwrite',
|
|
17
|
+
cancelLabel: 'Keep existing',
|
|
18
|
+
});
|
|
19
|
+
if (!shouldOverwrite) {
|
|
20
|
+
cancelPromptAndExit('Setup cancelled. Keeping existing configuration.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const runInitWizard = async () => {
|
|
25
|
+
const projectRoot = process.cwd();
|
|
26
|
+
if (!isProject(projectRoot)) {
|
|
27
|
+
cancelPromptAndExit('React Native Harness must be run in a React Native project root (directory with package.json containing react-native).');
|
|
28
|
+
}
|
|
29
|
+
intro('React Native Harness');
|
|
30
|
+
note("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.", 'Configuration wizard');
|
|
31
|
+
await checkForExistingConfig(projectRoot);
|
|
32
|
+
const projectConfig = await getProjectConfig();
|
|
33
|
+
const selectedPlatforms = await installPlatforms();
|
|
34
|
+
const selectedTargets = await discoverTargets(selectedPlatforms);
|
|
35
|
+
const bundleIds = await getBundleIds(selectedPlatforms);
|
|
36
|
+
generateConfig(projectConfig, selectedPlatforms, selectedTargets, bundleIds);
|
|
37
|
+
await createJestConfig(projectRoot);
|
|
38
|
+
note('A dedicated Jest configuration (jest.harness.config.mjs) has been created. Harness will automatically use it when you run the "harness" command.', 'Jest configuration');
|
|
39
|
+
outro('Setup complete. Happy testing!');
|
|
40
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jestConfig.d.ts","sourceRoot":"","sources":["../../src/wizard/jestConfig.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,gBAAgB,GAAU,aAAa,MAAM,kBAuBzD,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promptConfirm } from '@react-native-harness/tools';
|
|
4
|
+
export const createJestConfig = async (projectRoot) => {
|
|
5
|
+
const configPath = path.join(projectRoot, 'jest.harness.config.mjs');
|
|
6
|
+
if (fs.existsSync(configPath)) {
|
|
7
|
+
const shouldOverwrite = await promptConfirm({
|
|
8
|
+
message: 'A dedicated Jest configuration (jest.harness.config.mjs) already exists. Do you want to overwrite it?',
|
|
9
|
+
confirmLabel: 'Overwrite',
|
|
10
|
+
cancelLabel: 'Keep existing',
|
|
11
|
+
});
|
|
12
|
+
if (!shouldOverwrite) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const configContent = `export default {
|
|
17
|
+
preset: 'react-native-harness',
|
|
18
|
+
testMatch: ['<rootDir>/**/__tests__/**/*.harness.[jt]s?(x)'],
|
|
19
|
+
};
|
|
20
|
+
`;
|
|
21
|
+
fs.writeFileSync(configPath, configContent);
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jestIntegration.d.ts","sourceRoot":"","sources":["../../src/wizard/jestIntegration.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,YAgBpC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { note } from '@react-native-harness/tools';
|
|
2
|
+
export const showJestIntegrationNotes = () => {
|
|
3
|
+
note(`Update your jest.config.js to include the Harness project:
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
projects: [
|
|
7
|
+
{
|
|
8
|
+
displayName: 'Harness',
|
|
9
|
+
preset: 'react-native-harness',
|
|
10
|
+
testMatch: ['<rootDir>/**/__tests__/**/*.harness.[jt]s?(x)'],
|
|
11
|
+
},
|
|
12
|
+
// ...your other projects
|
|
13
|
+
],
|
|
14
|
+
};`, 'Next Steps');
|
|
15
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packageManager.d.ts","sourceRoot":"","sources":["../../src/wizard/packageManager.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,+BAKhC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const detectPackageManager = () => {
|
|
4
|
+
const cwd = process.cwd();
|
|
5
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml')))
|
|
6
|
+
return 'pnpm';
|
|
7
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock')))
|
|
8
|
+
return 'yarn';
|
|
9
|
+
return 'npm';
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platforms.d.ts","sourceRoot":"","sources":["../../src/wizard/platforms.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,gBAAgB,QAAa,OAAO,CAAC,MAAM,EAAE,CAoCzD,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { promptMultiselect, spinner, cancelPromptAndExit, installDevDependency, } from '@react-native-harness/tools';
|
|
2
|
+
export const installPlatforms = async () => {
|
|
3
|
+
const projectRoot = process.cwd();
|
|
4
|
+
const selectedPlatforms = await promptMultiselect({
|
|
5
|
+
message: 'Select platforms to support',
|
|
6
|
+
options: [
|
|
7
|
+
{ value: 'android', label: 'Android' },
|
|
8
|
+
{ value: 'ios', label: 'iOS' },
|
|
9
|
+
{ value: 'web', label: 'Web' },
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
if (selectedPlatforms.length === 0) {
|
|
13
|
+
cancelPromptAndExit('At least one platform must be selected.');
|
|
14
|
+
}
|
|
15
|
+
const installSpinner = spinner();
|
|
16
|
+
installSpinner.start('Installing platform packages...');
|
|
17
|
+
const packagesToInstall = [];
|
|
18
|
+
if (selectedPlatforms.includes('android'))
|
|
19
|
+
packagesToInstall.push('@react-native-harness/platform-android');
|
|
20
|
+
if (selectedPlatforms.includes('ios'))
|
|
21
|
+
packagesToInstall.push('@react-native-harness/platform-apple');
|
|
22
|
+
if (selectedPlatforms.includes('web'))
|
|
23
|
+
packagesToInstall.push('@react-native-harness/platform-web');
|
|
24
|
+
try {
|
|
25
|
+
await installDevDependency(projectRoot, packagesToInstall);
|
|
26
|
+
installSpinner.stop('Platform packages installed successfully.');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
installSpinner.stop('Failed to install platform packages.', 1);
|
|
30
|
+
console.error(error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return selectedPlatforms;
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectType.d.ts","sourceRoot":"","sources":["../../src/wizard/projectType.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,wBAAwB,EAAE,MAAM,CAAC;CAClC,CAAC;AAmBF,eAAO,MAAM,gBAAgB,QAAa,OAAO,CAAC,aAAa,CAkD9D,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promptSelect, promptText } from '@react-native-harness/tools';
|
|
4
|
+
const tryExtractComponentName = (entryPoint) => {
|
|
5
|
+
try {
|
|
6
|
+
const filePath = path.resolve(process.cwd(), entryPoint);
|
|
7
|
+
if (!fs.existsSync(filePath))
|
|
8
|
+
return undefined;
|
|
9
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
10
|
+
// Look for AppRegistry.registerComponent('Name', ...) or AppRegistry.registerComponent("Name", ...)
|
|
11
|
+
const match = content.match(/AppRegistry\.registerComponent\(\s*['"](.+?)['"]/);
|
|
12
|
+
return match ? match[1] : undefined;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const getProjectConfig = async () => {
|
|
19
|
+
const projectType = await promptSelect({
|
|
20
|
+
message: 'What type of project is this?',
|
|
21
|
+
options: [
|
|
22
|
+
{ value: 'expo', label: 'Expo' },
|
|
23
|
+
{ value: 'custom', label: 'React Native CLI / Custom' },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
if (projectType === 'expo') {
|
|
27
|
+
try {
|
|
28
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
29
|
+
return {
|
|
30
|
+
entryPoint: packageJson.main || './node_modules/expo/AppEntry.js',
|
|
31
|
+
appRegistryComponentName: 'main',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {
|
|
36
|
+
entryPoint: './node_modules/expo/AppEntry.js',
|
|
37
|
+
appRegistryComponentName: 'main',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const entryPoint = await promptText({
|
|
42
|
+
message: 'Enter the path to your app entry file',
|
|
43
|
+
initialValue: './index.js',
|
|
44
|
+
placeholder: './index.js',
|
|
45
|
+
validate: (value) => {
|
|
46
|
+
if (!value)
|
|
47
|
+
return 'Entry point is required';
|
|
48
|
+
return;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const suggestedComponentName = tryExtractComponentName(entryPoint);
|
|
52
|
+
const appRegistryComponentName = await promptText({
|
|
53
|
+
message: 'Enter the name of the component registered via AppRegistry.registerComponent()',
|
|
54
|
+
placeholder: 'MyAppName',
|
|
55
|
+
initialValue: suggestedComponentName,
|
|
56
|
+
validate: (value) => {
|
|
57
|
+
if (!value)
|
|
58
|
+
return 'Component name is required';
|
|
59
|
+
return;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return { entryPoint, appRegistryComponentName };
|
|
63
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../src/wizard/targets.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,eAAO,MAAM,eAAe,GAC1B,mBAAmB,MAAM,EAAE,KAC1B,OAAO,CAAC,SAAS,EAAE,CAoErB,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spinner, promptAutocompleteMultiselect, cancelPromptAndExit, } from '@react-native-harness/tools';
|
|
2
|
+
export const discoverTargets = async (selectedPlatforms) => {
|
|
3
|
+
const allTargets = [];
|
|
4
|
+
const targetSpinner = spinner();
|
|
5
|
+
targetSpinner.start('Discovering available targets...');
|
|
6
|
+
if (selectedPlatforms.includes('android')) {
|
|
7
|
+
try {
|
|
8
|
+
const androidPlatform = await import('@react-native-harness/platform-android');
|
|
9
|
+
const targets = await androidPlatform.getRunTargets();
|
|
10
|
+
allTargets.push(...targets);
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
console.error('Failed to load Android targets:', e);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (selectedPlatforms.includes('ios')) {
|
|
17
|
+
try {
|
|
18
|
+
const applePlatform = await import('@react-native-harness/platform-apple');
|
|
19
|
+
const targets = await applePlatform.getRunTargets();
|
|
20
|
+
allTargets.push(...targets);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.error('Failed to load iOS targets:', e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (selectedPlatforms.includes('web')) {
|
|
27
|
+
try {
|
|
28
|
+
const webPlatform = await import('@react-native-harness/platform-web');
|
|
29
|
+
const targets = await webPlatform.getRunTargets();
|
|
30
|
+
allTargets.push(...targets);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error('Failed to load Web targets:', e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
targetSpinner.stop('Target discovery complete.');
|
|
37
|
+
if (allTargets.length === 0) {
|
|
38
|
+
cancelPromptAndExit('No available targets (emulators or devices) found.');
|
|
39
|
+
}
|
|
40
|
+
const options = allTargets.map((target, index) => {
|
|
41
|
+
let platformLabel = 'Unknown';
|
|
42
|
+
if (target.platform === 'android')
|
|
43
|
+
platformLabel = 'Android';
|
|
44
|
+
else if (target.platform === 'ios')
|
|
45
|
+
platformLabel = 'iOS';
|
|
46
|
+
else if (target.platform === 'web')
|
|
47
|
+
platformLabel = 'Web';
|
|
48
|
+
return {
|
|
49
|
+
value: index,
|
|
50
|
+
label: `[${platformLabel}] ${target.name} (${target.type})`,
|
|
51
|
+
hint: target.description,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const selectedTargetIndices = await promptAutocompleteMultiselect({
|
|
55
|
+
message: 'Select targets to support in Harness',
|
|
56
|
+
options,
|
|
57
|
+
});
|
|
58
|
+
if (selectedTargetIndices.length === 0) {
|
|
59
|
+
cancelPromptAndExit('At least one target must be selected.');
|
|
60
|
+
}
|
|
61
|
+
return selectedTargetIndices.map((i) => allTargets[i]);
|
|
62
|
+
};
|
package/eslint.config.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export default [
|
|
|
9
9
|
'error',
|
|
10
10
|
{
|
|
11
11
|
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
|
|
12
|
-
ignoredDependencies: ['@react-native-harness/bridge'],
|
|
12
|
+
ignoredDependencies: ['@react-native-harness/bridge', '@react-native-harness/platform-android', '@react-native-harness/platform-apple', '@react-native-harness/platform-web'],
|
|
13
13
|
},
|
|
14
14
|
],
|
|
15
15
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/cli",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -22,11 +22,16 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"tslib": "^2.3.0",
|
|
25
|
-
"@react-native-harness/
|
|
26
|
-
"@react-native-harness/
|
|
25
|
+
"@react-native-harness/platforms": "1.0.0",
|
|
26
|
+
"@react-native-harness/bridge": "1.0.0",
|
|
27
|
+
"@react-native-harness/tools": "1.0.0",
|
|
28
|
+
"@react-native-harness/config": "1.0.0"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"jest-cli": "^30.2.0"
|
|
31
|
+
"jest-cli": "^30.2.0",
|
|
32
|
+
"@react-native-harness/platform-android": "1.0.0",
|
|
33
|
+
"@react-native-harness/platform-apple": "1.0.0",
|
|
34
|
+
"@react-native-harness/platform-web": "1.0.0"
|
|
30
35
|
},
|
|
31
36
|
"peerDependencies": {
|
|
32
37
|
"jest-cli": "*"
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { run, yargsOptions } from 'jest-cli';
|
|
2
2
|
import { getConfig } from '@react-native-harness/config';
|
|
3
|
+
import { runInitWizard } from './wizard/index.js';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const JEST_CONFIG_EXTENSIONS = ['.mjs', '.js', '.cjs'];
|
|
8
|
+
const JEST_HARNESS_CONFIG_BASE = 'jest.harness.config';
|
|
3
9
|
|
|
4
10
|
const checkForOldConfig = async () => {
|
|
5
11
|
try {
|
|
6
12
|
const { config } = await getConfig(process.cwd());
|
|
7
13
|
|
|
8
14
|
if (config.include) {
|
|
9
|
-
console.error('\n❌ Migration
|
|
15
|
+
console.error('\n❌ Migration required\n');
|
|
10
16
|
console.error('React Native Harness has migrated to the Jest CLI.');
|
|
11
17
|
console.error(
|
|
12
18
|
'The "include" property in your rn-harness.config file is no longer supported.\n'
|
|
@@ -27,7 +33,7 @@ const checkForOldConfig = async () => {
|
|
|
27
33
|
const patchYargsOptions = () => {
|
|
28
34
|
yargsOptions.harnessRunner = {
|
|
29
35
|
type: 'string',
|
|
30
|
-
description: 'Specify which
|
|
36
|
+
description: 'Specify which harness runner to use',
|
|
31
37
|
requiresArg: true,
|
|
32
38
|
};
|
|
33
39
|
|
|
@@ -67,5 +73,28 @@ const patchYargsOptions = () => {
|
|
|
67
73
|
delete yargsOptions.logHeapUsage;
|
|
68
74
|
};
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
if (process.argv.includes('init')) {
|
|
77
|
+
runInitWizard();
|
|
78
|
+
} else {
|
|
79
|
+
patchYargsOptions();
|
|
80
|
+
|
|
81
|
+
const hasConfigArg =
|
|
82
|
+
process.argv.includes('--config') || process.argv.includes('-c');
|
|
83
|
+
|
|
84
|
+
if (!hasConfigArg) {
|
|
85
|
+
const existingConfigExt = JEST_CONFIG_EXTENSIONS.find((ext) =>
|
|
86
|
+
fs.existsSync(
|
|
87
|
+
path.join(process.cwd(), `${JEST_HARNESS_CONFIG_BASE}${ext}`)
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (existingConfigExt) {
|
|
92
|
+
process.argv.push(
|
|
93
|
+
'--config',
|
|
94
|
+
`${JEST_HARNESS_CONFIG_BASE}${existingConfigExt}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
checkForOldConfig().then(() => run());
|
|
100
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { promptText } from '@react-native-harness/tools';
|
|
2
|
+
|
|
3
|
+
export const getBundleIds = async (
|
|
4
|
+
selectedPlatforms: string[]
|
|
5
|
+
): Promise<Record<string, string>> => {
|
|
6
|
+
const bundleIds: Record<string, string> = {};
|
|
7
|
+
|
|
8
|
+
if (selectedPlatforms.includes('android')) {
|
|
9
|
+
bundleIds.android = await promptText({
|
|
10
|
+
message: 'Enter Android package name',
|
|
11
|
+
placeholder: 'com.example.app',
|
|
12
|
+
validate: (value: string | undefined) => {
|
|
13
|
+
if (!value) return 'Package name is required';
|
|
14
|
+
const parts = value.split('.');
|
|
15
|
+
if (parts.length < 2) {
|
|
16
|
+
return 'Package name must have at least two segments (e.g., com.example)';
|
|
17
|
+
}
|
|
18
|
+
for (const segment of parts) {
|
|
19
|
+
if (!segment) return 'Segments cannot be empty';
|
|
20
|
+
if (!/^[a-zA-Z]/.test(segment)) {
|
|
21
|
+
return `Segment "${segment}" must start with a letter`;
|
|
22
|
+
}
|
|
23
|
+
if (!/^[a-zA-Z0-9_]+$/.test(segment)) {
|
|
24
|
+
return `Segment "${segment}" can only contain alphanumeric characters or underscores`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (selectedPlatforms.includes('ios')) {
|
|
33
|
+
bundleIds.ios = await promptText({
|
|
34
|
+
message: 'Enter iOS bundle identifier',
|
|
35
|
+
placeholder: 'com.example.app',
|
|
36
|
+
validate: (value: string | undefined) => {
|
|
37
|
+
if (!value) return 'Bundle identifier is required';
|
|
38
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(value)) {
|
|
39
|
+
return 'Bundle identifier can only contain alphanumeric characters, hyphens, and periods';
|
|
40
|
+
}
|
|
41
|
+
if (value.startsWith('.') || value.endsWith('.')) {
|
|
42
|
+
return 'Bundle identifier cannot start or end with a period';
|
|
43
|
+
}
|
|
44
|
+
if (value.includes('..')) {
|
|
45
|
+
return 'Bundle identifier cannot contain consecutive periods';
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (selectedPlatforms.includes('web')) {
|
|
53
|
+
bundleIds.web = await promptText({
|
|
54
|
+
message: 'Enter application URL',
|
|
55
|
+
initialValue: 'http://localhost:8081/index.html',
|
|
56
|
+
placeholder: 'http://localhost:8081/index.html',
|
|
57
|
+
validate: (value: string | undefined) => {
|
|
58
|
+
if (!value) return 'URL is required';
|
|
59
|
+
try {
|
|
60
|
+
new URL(value);
|
|
61
|
+
return;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return 'Please enter a valid URL';
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return bundleIds;
|
|
70
|
+
};
|