Skip to content

Commit 60deb15

Browse files
authored
feat: auto-inject Harness (#8)
The changes required for Harness to run in the app should be minimal. This pull request updates the way Harness is injected into the app, making it non-intrusive - no changes to the app's code are required.
1 parent cae7174 commit 60deb15

17 files changed

Lines changed: 149 additions & 103 deletions

File tree

apps/playground/react-native-harness.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/playground/rn-harness.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const config = {
2+
entryPoint: './src/main.tsx',
3+
appRegistryComponentName: 'Playground',
24
include: ['./src/__tests__/**/*'],
35

46
runners: [

apps/playground/src/main.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import { AppRegistry } from 'react-native';
2+
import App from './app/App';
23

3-
AppRegistry.registerComponent('Playground', () =>
4-
global.RN_HARNESS
5-
? require('react-native-harness').ReactNativeHarness
6-
: require('./app/App').default
7-
);
4+
AppRegistry.registerComponent('Playground', () => App);

packages/babel-preset/src/global-plugin.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import globalRnHarnessPlugin from './global-plugin';
21
import resolveWeakPlugin from './resolve-weak-plugin';
32

43
export const rnHarnessPreset = () => {
5-
// Unfortunately, the Babel preset must be enabled at all times (at least for now).
4+
if (!process.env.RN_HARNESS) {
5+
return {};
6+
}
7+
68
return {
7-
plugins: [
8-
'@babel/plugin-transform-class-static-block',
9-
globalRnHarnessPlugin,
10-
resolveWeakPlugin,
11-
],
9+
plugins: ['@babel/plugin-transform-class-static-block', resolveWeakPlugin],
1210
};
1311
};

packages/config/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export const TestRunnerConfigSchema = z.discriminatedUnion('platform', [
5959

6060
export const ConfigSchema = z
6161
.object({
62+
entryPoint: z.string().min(1, 'Entry point is required'),
63+
appRegistryComponentName: z
64+
.string()
65+
.min(1, 'App registry component name is required'),
6266
include: z.union([z.string(), z.array(z.string())]).refine(
6367
(val) => {
6468
if (Array.isArray(val)) {

packages/metro/assets/init.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/metro/src/manifest.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import path from 'node:path';
2+
import fs from 'node:fs';
3+
import { Config as HarnessConfig } from '@react-native-harness/config';
4+
5+
const getManifestContent = (harnessConfig: HarnessConfig): string => {
6+
return `global.RN_HARNESS = { appRegistryComponentName: '${harnessConfig.appRegistryComponentName}' };`;
7+
};
8+
9+
export const getHarnessManifest = (harnessConfig: HarnessConfig): string => {
10+
const manifestContent = getManifestContent(harnessConfig);
11+
const manifestPath = path.resolve(
12+
process.cwd(),
13+
'node_modules/.cache/rn-harness/manifest.js'
14+
);
15+
16+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
17+
fs.writeFileSync(manifestPath, manifestContent);
18+
19+
return manifestPath;
20+
};

packages/metro/src/resolver.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { MetroConfig } from '@react-native/metro-config';
2+
import type { Config as HarnessConfig } from '@react-native-harness/config';
3+
4+
type CustomResolver = NonNullable<
5+
NonNullable<MetroConfig['resolver']>['resolveRequest']
6+
>;
7+
8+
export const getHarnessResolver = (
9+
metroConfig: MetroConfig,
10+
harnessConfig: HarnessConfig
11+
): CustomResolver => {
12+
// Can be relative to the project root or absolute, need to normalize it
13+
const resolvedEntryPointPath = require.resolve(harnessConfig.entryPoint, {
14+
paths: [process.cwd()],
15+
});
16+
17+
return (context, moduleName, platform) => {
18+
const existingResolver =
19+
metroConfig.resolver?.resolveRequest ?? context.resolveRequest;
20+
const resolvedModule = existingResolver(context, moduleName, platform);
21+
22+
// Replace the entry point with Harness
23+
if (
24+
resolvedModule.type === 'sourceFile' &&
25+
resolvedModule.filePath === resolvedEntryPointPath
26+
) {
27+
return {
28+
type: 'sourceFile',
29+
filePath: require.resolve('@react-native-harness/runtime/entry-point'),
30+
};
31+
}
32+
33+
return resolvedModule;
34+
};
35+
};

packages/metro/src/withRnHarness.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { MetroConfig } from '@react-native/metro-config';
22
import { getConfig } from '@react-native-harness/config';
33
import { patchModuleSystem } from './moduleSystem';
4-
5-
export type RnHarnessOptions = {
6-
unstable__skipAlreadyIncludedModules?: boolean;
7-
};
4+
import { getHarnessResolver } from './resolver';
5+
import { getHarnessManifest } from './manifest';
86

97
export const withRnHarness = async (
108
config: MetroConfig | Promise<MetroConfig>
@@ -20,20 +18,24 @@ export const withRnHarness = async (
2018

2119
patchModuleSystem();
2220

21+
const harnessResolver = getHarnessResolver(metroConfig, harnessConfig);
22+
const harnessManifest = getHarnessManifest(harnessConfig);
23+
2324
const patchedConfig: MetroConfig = {
2425
...metroConfig,
2526
cacheVersion: 'react-native-harness',
2627
serializer: {
2728
...metroConfig.serializer,
2829
getPolyfills: (...args) => [
2930
...(metroConfig.serializer?.getPolyfills?.(...args) ?? []),
30-
require.resolve('../assets/init.js'),
31+
harnessManifest,
3132
],
3233
},
3334
resolver: {
3435
...metroConfig.resolver,
3536
// Unlock __tests__ directory
3637
blockList: undefined,
38+
resolveRequest: harnessResolver,
3739
},
3840
};
3941

0 commit comments

Comments
 (0)