@react-native-harness/metro 1.1.0-rc.1 → 1.1.0-rc.3

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.
@@ -1,84 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.withRnHarness = void 0;
4
- const config_1 = require("@react-native-harness/config");
5
- const resolver_1 = require("./resolvers/resolver");
6
- const manifest_1 = require("./manifest");
7
- const babel_transformer_1 = require("./babel-transformer");
8
- const metro_cache_1 = require("./metro-cache");
9
- const INTERNAL_CALLSITES_REGEX = /(^|[\\/])(node_modules[/\\]@react-native-harness)([\\/]|$)/;
10
- const withRnHarness = (config, isInvokedByHarness = false) => {
11
- // This is a workaround for a regression in Metro 0.83, when promises are not handled correctly.
4
+ let hasWarned = false;
5
+ const withRnHarness = (config, _isInvokedByHarness = false) => {
12
6
  return async () => {
13
- // If the function is not invoked by the Harness, return the config as is.
14
- // We'll remove it in the next major version.
15
- if (!isInvokedByHarness) {
16
- return config;
7
+ if (!hasWarned) {
8
+ hasWarned = true;
9
+ console.warn("[react-native-harness] `withRnHarness` in Metro configs is deprecated and will be removed in a future release. Remove `withRnHarness` from your Metro config; React Native Harness now patches Metro internally.");
17
10
  }
18
- const metroConfig = await config;
19
- const { config: harnessConfig } = await (0, config_1.getConfig)(process.cwd());
20
- const harnessResolver = (0, resolver_1.getHarnessResolver)(metroConfig, harnessConfig);
21
- const harnessManifest = (0, manifest_1.getHarnessManifest)(harnessConfig);
22
- const harnessBabelTransformerPath = (0, babel_transformer_1.getHarnessBabelTransformerPath)(metroConfig);
23
- const patchedConfig = {
24
- ...metroConfig,
25
- cacheVersion: 'react-native-harness',
26
- server: {
27
- ...metroConfig.server,
28
- forwardClientLogs: harnessConfig.forwardClientLogs ?? false,
29
- },
30
- serializer: {
31
- ...metroConfig.serializer,
32
- getPolyfills: (...args) => [
33
- ...(metroConfig.serializer?.getPolyfills?.(...args) ?? []),
34
- harnessManifest,
35
- require.resolve('@react-native-harness/runtime/polyfills/harness-module-system'),
36
- ],
37
- isThirdPartyModule({ path: modulePath }) {
38
- const isThirdPartyByDefault = metroConfig.serializer?.isThirdPartyModule?.({
39
- path: modulePath,
40
- }) ?? false;
41
- if (isThirdPartyByDefault) {
42
- return true;
43
- }
44
- return INTERNAL_CALLSITES_REGEX.test(modulePath);
45
- },
46
- },
47
- resolver: {
48
- ...metroConfig.resolver,
49
- // Unlock __tests__ directory
50
- blockList: undefined,
51
- resolveRequest: harnessResolver,
52
- },
53
- transformer: {
54
- ...metroConfig.transformer,
55
- babelTransformerPath: harnessBabelTransformerPath,
56
- },
57
- symbolicator: {
58
- ...metroConfig.symbolicator,
59
- customizeFrame: async (frame) => {
60
- const defaultCustomizeFrame = await metroConfig.symbolicator?.customizeFrame?.(frame);
61
- const shouldCollapseByDefault = defaultCustomizeFrame?.collapse ?? false;
62
- if (shouldCollapseByDefault) {
63
- return {
64
- collapse: true,
65
- };
66
- }
67
- return {
68
- collapse: frame.file != null && INTERNAL_CALLSITES_REGEX.test(frame.file),
69
- };
70
- },
71
- },
72
- };
73
- if (harnessConfig.unstable__enableMetroCache) {
74
- patchedConfig.cacheStores =
75
- (0, metro_cache_1.getHarnessCacheStores)();
76
- }
77
- if (harnessConfig.unstable__skipAlreadyIncludedModules) {
78
- patchedConfig.serializer.customSerializer =
79
- require('./getHarnessSerializer').getHarnessSerializer();
80
- }
81
- return patchedConfig;
11
+ return await config;
82
12
  };
83
13
  };
84
14
  exports.withRnHarness = withRnHarness;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-harness/metro",
3
- "version": "1.1.0-rc.1",
3
+ "version": "1.1.0-rc.3",
4
4
  "type": "commonjs",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -15,18 +15,13 @@
15
15
  }
16
16
  },
17
17
  "peerDependencies": {
18
- "metro": "*",
19
- "@react-native-harness/runtime": "1.1.0-rc.1"
18
+ "metro": "*"
20
19
  },
21
20
  "dependencies": {
22
- "tslib": "^2.3.0",
23
- "@react-native-harness/config": "1.1.0-rc.1",
24
- "@react-native-harness/babel-preset": "1.1.0-rc.1"
21
+ "tslib": "^2.3.0"
25
22
  },
26
23
  "devDependencies": {
27
- "@types/babel__core": "^7.20.5",
28
- "metro": "*",
29
- "@react-native-harness/runtime": "1.1.0-rc.1"
24
+ "metro": "*"
30
25
  },
31
26
  "license": "MIT"
32
27
  }
@@ -0,0 +1,32 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const consoleWarnSpy = vi
4
+ .spyOn(console, 'warn')
5
+ .mockImplementation(() => undefined);
6
+
7
+ afterEach(() => {
8
+ consoleWarnSpy.mockClear();
9
+ vi.resetModules();
10
+ });
11
+
12
+ describe('withRnHarness', () => {
13
+ it('returns the provided config unchanged', async () => {
14
+ const { withRnHarness } = await import('../withRnHarness.js');
15
+ const config = { resolver: { blockList: [] } };
16
+
17
+ await expect(withRnHarness(config)()).resolves.toBe(config);
18
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
19
+ });
20
+
21
+ it('warns only once across repeated calls', async () => {
22
+ const { withRnHarness } = await import('../withRnHarness.js');
23
+
24
+ await withRnHarness({ projectRoot: '/tmp/app' }, true)();
25
+ await withRnHarness(Promise.resolve({ projectRoot: '/tmp/app' }), true)();
26
+
27
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
28
+ expect(consoleWarnSpy.mock.calls[0]?.[0]).toContain(
29
+ 'Remove `withRnHarness` from your Metro config'
30
+ );
31
+ });
32
+ });
@@ -1,109 +1,17 @@
1
- import type { MetroConfig } from 'metro-config';
2
- import { getConfig } from '@react-native-harness/config';
3
- import { getHarnessResolver } from './resolvers/resolver';
4
- import { getHarnessManifest } from './manifest';
5
- import { getHarnessBabelTransformerPath } from './babel-transformer';
6
- import { getHarnessCacheStores } from './metro-cache';
7
- import type { NotReadOnly } from './utils';
1
+ let hasWarned = false;
8
2
 
9
- const INTERNAL_CALLSITES_REGEX =
10
- /(^|[\\/])(node_modules[/\\]@react-native-harness)([\\/]|$)/;
11
-
12
- export const withRnHarness = <T extends MetroConfig>(
3
+ export const withRnHarness = <T>(
13
4
  config: T | Promise<T>,
14
- isInvokedByHarness = false
5
+ _isInvokedByHarness = false
15
6
  ): (() => Promise<T>) => {
16
- // This is a workaround for a regression in Metro 0.83, when promises are not handled correctly.
17
7
  return async () => {
18
- // If the function is not invoked by the Harness, return the config as is.
19
- // We'll remove it in the next major version.
20
- if (!isInvokedByHarness) {
21
- return config;
22
- }
23
-
24
- const metroConfig = await config;
25
- const { config: harnessConfig } = await getConfig(process.cwd());
26
-
27
- const harnessResolver = getHarnessResolver(metroConfig, harnessConfig);
28
- const harnessManifest = getHarnessManifest(harnessConfig);
29
- const harnessBabelTransformerPath =
30
- getHarnessBabelTransformerPath(metroConfig);
31
-
32
- const patchedConfig: MetroConfig = {
33
- ...metroConfig,
34
- cacheVersion: 'react-native-harness',
35
- server: {
36
- ...metroConfig.server,
37
- forwardClientLogs: harnessConfig.forwardClientLogs ?? false,
38
- },
39
- serializer: {
40
- ...metroConfig.serializer,
41
- getPolyfills: (...args) => [
42
- ...(metroConfig.serializer?.getPolyfills?.(...args) ?? []),
43
- harnessManifest,
44
- require.resolve(
45
- '@react-native-harness/runtime/polyfills/harness-module-system'
46
- ),
47
- ],
48
- isThirdPartyModule({ path: modulePath }) {
49
- const isThirdPartyByDefault =
50
- metroConfig.serializer?.isThirdPartyModule?.({
51
- path: modulePath,
52
- }) ?? false;
53
-
54
- if (isThirdPartyByDefault) {
55
- return true;
56
- }
57
-
58
- return INTERNAL_CALLSITES_REGEX.test(modulePath);
59
- },
60
- },
61
- resolver: {
62
- ...metroConfig.resolver,
63
- // Unlock __tests__ directory
64
- blockList: undefined,
65
- resolveRequest: harnessResolver,
66
- },
67
- transformer: {
68
- ...metroConfig.transformer,
69
- babelTransformerPath: harnessBabelTransformerPath,
70
- },
71
- symbolicator: {
72
- ...metroConfig.symbolicator,
73
- customizeFrame: async (frame) => {
74
- const defaultCustomizeFrame =
75
- await metroConfig.symbolicator?.customizeFrame?.(frame);
76
- const shouldCollapseByDefault =
77
- defaultCustomizeFrame?.collapse ?? false;
78
-
79
- if (shouldCollapseByDefault) {
80
- return {
81
- collapse: true,
82
- };
83
- }
84
-
85
- return {
86
- collapse:
87
- frame.file != null && INTERNAL_CALLSITES_REGEX.test(frame.file),
88
- };
89
- },
90
- },
91
- };
92
-
93
- if (harnessConfig.unstable__enableMetroCache) {
94
- (patchedConfig.cacheStores as NotReadOnly<MetroConfig['cacheStores']>) =
95
- getHarnessCacheStores();
96
- }
97
-
98
- if (harnessConfig.unstable__skipAlreadyIncludedModules) {
99
- (
100
- patchedConfig.serializer as NonNullable<
101
- NotReadOnly<MetroConfig['serializer']>
102
- >
103
- ).customSerializer =
104
- require('./getHarnessSerializer').getHarnessSerializer();
8
+ if (!hasWarned) {
9
+ hasWarned = true;
10
+ console.warn(
11
+ "[react-native-harness] `withRnHarness` in Metro configs is deprecated and will be removed in a future release. Remove `withRnHarness` from your Metro config; React Native Harness now patches Metro internally."
12
+ );
105
13
  }
106
14
 
107
- return patchedConfig as T;
15
+ return await config;
108
16
  };
109
17
  };
package/tsconfig.json CHANGED
@@ -3,15 +3,6 @@
3
3
  "files": [],
4
4
  "include": [],
5
5
  "references": [
6
- {
7
- "path": "../babel-preset"
8
- },
9
- {
10
- "path": "../config"
11
- },
12
- {
13
- "path": "../runtime"
14
- },
15
6
  {
16
7
  "path": "./tsconfig.lib.json"
17
8
  }
package/tsconfig.lib.json CHANGED
@@ -9,16 +9,5 @@
9
9
  "forceConsistentCasingInFileNames": true,
10
10
  "types": ["node"]
11
11
  },
12
- "include": ["src/**/*.ts"],
13
- "references": [
14
- {
15
- "path": "../babel-preset/tsconfig.lib.json"
16
- },
17
- {
18
- "path": "../config/tsconfig.lib.json"
19
- },
20
- {
21
- "path": "../runtime/tsconfig.lib.json"
22
- }
23
- ]
12
+ "include": ["src/**/*.ts"]
24
13
  }
package/vite.config.ts ADDED
@@ -0,0 +1,18 @@
1
+ /// <reference types='vitest' />
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig(() => ({
5
+ root: __dirname,
6
+ cacheDir: '../../node_modules/.vite/packages/metro',
7
+ test: {
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
12
+ reporters: ['default'],
13
+ coverage: {
14
+ reportsDirectory: './test-output/vitest/coverage',
15
+ provider: 'v8' as const,
16
+ },
17
+ },
18
+ }));
@@ -1,40 +0,0 @@
1
- import type { BabelTransformer } from 'metro-babel-transformer';
2
- import { rnHarnessPlugins } from '@react-native-harness/babel-preset';
3
- import { MetroConfig } from '@react-native/metro-config';
4
-
5
- export const getHarnessBabelTransformerPath = (
6
- metroConfig: MetroConfig
7
- ): string => {
8
- const upstreamTransformerPath = metroConfig.transformer?.babelTransformerPath;
9
-
10
- if (!upstreamTransformerPath || typeof upstreamTransformerPath !== 'string') {
11
- throw new Error('Upstream transformer path is not a string');
12
- }
13
-
14
- process.env.RN_HARNESS_UPSTREAM_TRANSFORMER_PATH = upstreamTransformerPath;
15
- return require.resolve('./babel-transformer.js');
16
- };
17
-
18
- const transform: BabelTransformer['transform'] = (args) => {
19
- const { plugins } = args;
20
- const upstreamTransformerPath =
21
- process.env.RN_HARNESS_UPSTREAM_TRANSFORMER_PATH;
22
-
23
- if (!upstreamTransformerPath || typeof upstreamTransformerPath !== 'string') {
24
- throw new Error('Upstream transformer path is not a string');
25
- }
26
-
27
- const upstreamTransformer = require(upstreamTransformerPath);
28
- const pluginsWithHarness = [
29
- // Checked against @babel/core's type definitions - plugins are an array of PluginItem
30
- ...((plugins as unknown[]) ?? []),
31
- ...rnHarnessPlugins,
32
- ];
33
-
34
- return upstreamTransformer.transform({
35
- ...args,
36
- plugins: pluginsWithHarness,
37
- });
38
- };
39
-
40
- export { transform };
package/src/errors.ts DELETED
@@ -1,6 +0,0 @@
1
- export class CouldNotPatchModuleSystemError extends Error {
2
- constructor() {
3
- super('Could not patch module system');
4
- this.name = 'CouldNotPatchModuleSystemError';
5
- }
6
- }
@@ -1,46 +0,0 @@
1
- import type { MetroConfig } from 'metro-config';
2
-
3
- export type Serializer = NonNullable<
4
- NonNullable<MetroConfig['serializer']>['customSerializer']
5
- >;
6
-
7
- const getBaseSerializer = (): Serializer => {
8
- const baseJSBundle = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
9
- const bundleToString = require('metro/private/lib/bundleToString');
10
-
11
- return (entryPoint, prepend, graph, bundleOptions) =>
12
- bundleToString(baseJSBundle(entryPoint, prepend, graph, bundleOptions));
13
- };
14
-
15
- const getAllFiles = require('metro/private/DeltaBundler/Serializers/getAllFiles');
16
-
17
- export const getHarnessSerializer = (): Serializer => {
18
- const baseSerializer = getBaseSerializer();
19
- let mainEntryPointModules = new Set<string>();
20
-
21
- return async (entryPoint, preModules, graph, options) => {
22
- if (options.modulesOnly) {
23
- // This is most likely a test file
24
- return baseSerializer(entryPoint, preModules, graph, {
25
- ...options,
26
- processModuleFilter: (mod) => {
27
- if (
28
- options.processModuleFilter &&
29
- !options.processModuleFilter(mod)
30
- ) {
31
- // If the module is not allowed by the processModuleFilter, skip it
32
- return false;
33
- }
34
-
35
- // If the module is in the main entry point, skip it
36
- return !mainEntryPointModules.has(mod.path);
37
- },
38
- });
39
- }
40
-
41
- mainEntryPointModules = new Set(
42
- await getAllFiles(preModules, graph, options)
43
- );
44
- return baseSerializer(entryPoint, preModules, graph, options);
45
- };
46
- };
@@ -1,6 +0,0 @@
1
- // Mock module for @jest/globals imports
2
- // This module throws immediately when imported to warn users about using Jest APIs
3
-
4
- throw new Error(
5
- "Importing '@jest/globals' is not supported in Harness tests. Import from 'react-native-harness' instead."
6
- );
package/src/manifest.ts DELETED
@@ -1,24 +0,0 @@
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 = {
7
- appRegistryComponentName: '${harnessConfig.appRegistryComponentName}',
8
- webSocketPort: ${harnessConfig.webSocketPort},
9
- disableViewFlattening: ${harnessConfig.disableViewFlattening},
10
- };`;
11
- };
12
-
13
- export const getHarnessManifest = (harnessConfig: HarnessConfig): string => {
14
- const manifestContent = getManifestContent(harnessConfig);
15
- const manifestPath = path.resolve(
16
- process.cwd(),
17
- 'node_modules/.cache/rn-harness/manifest.js'
18
- );
19
-
20
- fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
21
- fs.writeFileSync(manifestPath, manifestContent);
22
-
23
- return manifestPath;
24
- };
@@ -1,24 +0,0 @@
1
- import { CacheStore, MetroCache } from 'metro-cache';
2
- import type { MixedOutput, TransformResult } from 'metro';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import type { CacheStoresConfigT } from 'metro-config';
6
-
7
- const CACHE_ROOT = path.resolve(
8
- process.cwd(),
9
- 'node_modules/.cache/rn-harness/metro-cache'
10
- );
11
-
12
- export const getHarnessCacheStores = (): ((
13
- metroCache: MetroCache
14
- ) => CacheStoresConfigT) => {
15
- return ({ FileStore }) => {
16
- fs.mkdirSync(CACHE_ROOT, { recursive: true });
17
-
18
- return [
19
- new FileStore({ root: CACHE_ROOT }) as CacheStore<
20
- TransformResult<MixedOutput>
21
- >,
22
- ];
23
- };
24
- };
@@ -1,16 +0,0 @@
1
- import type { HarnessResolver, MetroResolver } from './types';
2
-
3
- export const createHarnessResolver = (
4
- resolvers: HarnessResolver[]
5
- ): MetroResolver => {
6
- return (context, moduleName, platform) => {
7
- for (const resolver of resolvers) {
8
- const result = resolver(context, moduleName, platform);
9
- if (result != null) {
10
- return result;
11
- }
12
- }
13
-
14
- return context.resolveRequest(context, moduleName, platform);
15
- };
16
- };
@@ -1,100 +0,0 @@
1
- import type { MetroConfig } from '@react-native/metro-config';
2
- import type { Config as HarnessConfig } from '@react-native-harness/config';
3
- import path from 'node:path';
4
- import { createHarnessResolver } from './composite-resolver';
5
- import { createTsConfigResolver } from './tsconfig-resolver';
6
- import type { HarnessResolver, MetroResolver } from './types';
7
-
8
- // Safely resolves a path and strips its extension
9
- const getExtensionlessAbsolutePath = (basePath: string, relativePath = ''): string => {
10
- const fullPath = path.resolve(basePath, relativePath);
11
- const parsed = path.parse(fullPath);
12
- return path.join(parsed.dir, parsed.name);
13
- }
14
-
15
- export const createHarnessEntryPointResolver = (harnessConfig: HarnessConfig): HarnessResolver => {
16
- const rootPath = path.resolve(process.cwd());
17
- const expectedEntryPoint = getExtensionlessAbsolutePath(rootPath, harnessConfig.entryPoint);
18
- const resolvedHarnessPath = require.resolve('@react-native-harness/runtime/entry-point');
19
-
20
- return (context, moduleName, _platform) => {
21
- // 1. Resolve the origin path of the file making the import
22
- const currentOrigin = path.resolve(context.originModulePath);
23
-
24
- // Fast Fail: If the import isn't happening from the root directory, skip it immediately
25
- if (currentOrigin !== rootPath) {
26
- return null;
27
- }
28
-
29
- // 2. Resolve the module being imported and strip its extension
30
- // This safely normalizes './index', './index.js', 'index.js', etc.
31
- const requestedModule = getExtensionlessAbsolutePath(currentOrigin, moduleName);
32
-
33
- // 3. String comparison
34
- if (requestedModule === expectedEntryPoint) {
35
- return {
36
- type: 'sourceFile',
37
- filePath: resolvedHarnessPath,
38
- };
39
- }
40
-
41
- return null;
42
- };
43
- };
44
-
45
- export const createJestGlobalsResolver = (): HarnessResolver => {
46
- return (_context, moduleName, _platform) => {
47
- // Intercept @jest/globals imports and redirect to mock module
48
- if (moduleName === '@jest/globals') {
49
- return {
50
- type: 'sourceFile',
51
- filePath: require.resolve('../jest-globals-mock'),
52
- };
53
- }
54
-
55
- return null;
56
- };
57
- };
58
-
59
- export const createJsxRuntimeResolver = (): HarnessResolver => {
60
- const resolvedJsxRuntimePath = require.resolve(
61
- '@react-native-harness/runtime/jsx-runtime'
62
- );
63
- const resolvedJsxDevRuntimePath = require.resolve(
64
- '@react-native-harness/runtime/jsx-dev-runtime'
65
- );
66
-
67
- return (_context, moduleName, _platform) => {
68
- if (moduleName === '@react-native-harness/runtime/jsx-runtime') {
69
- return {
70
- type: 'sourceFile',
71
- filePath: resolvedJsxRuntimePath,
72
- };
73
- }
74
-
75
- if (moduleName === '@react-native-harness/runtime/jsx-dev-runtime') {
76
- return {
77
- type: 'sourceFile',
78
- filePath: resolvedJsxDevRuntimePath,
79
- };
80
- }
81
-
82
- return null;
83
- };
84
- };
85
-
86
- export const getHarnessResolver = (
87
- metroConfig: MetroConfig,
88
- harnessConfig: HarnessConfig
89
- ): MetroResolver => {
90
- const userResolver = metroConfig.resolver?.resolveRequest;
91
- const resolvers: HarnessResolver[] = [
92
- createHarnessEntryPointResolver(harnessConfig),
93
- createJestGlobalsResolver(),
94
- createJsxRuntimeResolver(),
95
- createTsConfigResolver(process.cwd()),
96
- userResolver,
97
- ].filter((resolver): resolver is HarnessResolver => !!resolver);
98
-
99
- return createHarnessResolver(resolvers);
100
- };