Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/bundler-metro/babel-transformer.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { rnHarnessPlugins } = require('@react-native-harness/babel-preset');

const transform = (args) => {
const { plugins } = args;
const upstreamTransformerPath =
process.env.RN_HARNESS_UPSTREAM_TRANSFORMER_PATH;

if (!upstreamTransformerPath || typeof upstreamTransformerPath !== 'string') {
throw new Error('Upstream transformer path is not a string');
}

const upstreamTransformer = require(upstreamTransformerPath);
const pluginsWithHarness = [
...((plugins ?? [])),
...rnHarnessPlugins,
];

return upstreamTransformer.transform({
...args,
plugins: pluginsWithHarness,
});
};

module.exports = { transform };
1 change: 1 addition & 0 deletions packages/bundler-metro/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
ignoredDependencies: ['@react-native-harness/babel-preset', 'vitest'],
},
],
},
Expand Down
12 changes: 9 additions & 3 deletions packages/bundler-metro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,27 @@
}
},
"dependencies": {
"@react-native-harness/metro": "workspace:*",
"@react-native/metro-config": "*",
"@react-native-harness/babel-preset": "workspace:*",
"@react-native-harness/tools": "workspace:*",
"@react-native-harness/config": "workspace:*",
"@react-native-harness/runtime": "workspace:*",
"connect": "^3.7.0",
"nocache": "^4.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"metro": "*",
"metro-config": "*"
"metro-cache": "*",
"metro-config": "*",
"metro-resolver": "*"
},
"devDependencies": {
"@types/connect": "^3.4.38",
"metro": "*",
"metro-config": "*"
"metro-cache": "*",
"metro-config": "*",
"metro-resolver": "*"
},
"license": "MIT"
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
const tempDirs: string[] = [];

const createTempProjectRoot = (): string => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-harness-metro-'));
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'rn-harness-bundler-metro-')
);
tempDirs.push(tempDir);
return tempDir;
};
Expand All @@ -23,7 +25,7 @@ afterEach(() => {
}
});

describe('metro paths', () => {
describe('bundler metro paths', () => {
it('resolves the harness root under the project root', () => {
const projectRoot = createTempProjectRoot();

Expand Down
16 changes: 16 additions & 0 deletions packages/bundler-metro/src/babel-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { fileURLToPath } from 'node:url';
import { MetroConfig } from '@react-native/metro-config';

export const getHarnessBabelTransformerPath = (
metroConfig: MetroConfig
): string => {
const upstreamTransformerPath = metroConfig.transformer?.babelTransformerPath;

if (!upstreamTransformerPath || typeof upstreamTransformerPath !== 'string') {
throw new Error('Upstream transformer path is not a string');
}

process.env.RN_HARNESS_UPSTREAM_TRANSFORMER_PATH = upstreamTransformerPath;

return fileURLToPath(new URL('../babel-transformer.cjs', import.meta.url));
};
2 changes: 1 addition & 1 deletion packages/bundler-metro/src/factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { withRnHarness } from '@react-native-harness/metro';
import { logger } from '@react-native-harness/tools';
import type { Server as HttpServer } from 'node:http';
import type { Server as HttpsServer } from 'node:https';
Expand All @@ -15,6 +14,7 @@ import {
} from './reporter.js';
import { getExpoMiddleware } from './middlewares/expo-middleware.js';
import { getStatusMiddleware } from './middlewares/status-middleware.js';
import { withRnHarness } from './withRnHarness.js';

const waitForBundler = async (
reporter: Reporter,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { createRequire } from 'node:module';
import type { MetroConfig } from 'metro-config';

const require = createRequire(import.meta.url);

export type Serializer = NonNullable<
NonNullable<MetroConfig['serializer']>['customSerializer']
>;

const getBaseSerializer = (): Serializer => {
const baseJSBundle = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
const baseJSBundle = require(
'metro/private/DeltaBundler/Serializers/baseJSBundle'
);
const bundleToString = require('metro/private/lib/bundleToString');

return (entryPoint, prepend, graph, bundleOptions) =>
Expand All @@ -20,19 +25,16 @@ export const getHarnessSerializer = (): Serializer => {

return async (entryPoint, preModules, graph, options) => {
if (options.modulesOnly) {
// This is most likely a test file
return baseSerializer(entryPoint, preModules, graph, {
...options,
processModuleFilter: (mod) => {
if (
options.processModuleFilter &&
!options.processModuleFilter(mod)
) {
// If the module is not allowed by the processModuleFilter, skip it
return false;
}

// If the module is in the main entry point, skip it
return !mainEntryPointModules.has(mod.path);
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/bundler-metro/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { getMetroInstance } from './factory.js';
export type { MetroInstance, MetroFactory, MetroOptions } from './types.js';
export { prewarmMetroBundle } from './prewarm.js';
export type { Reporter, ReportableEvent } from './reporter.js';
export { isMetroCacheReusable } from './paths.js';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Mock module for @jest/globals imports
// This module throws immediately when imported to warn users about using Jest APIs
// This module throws immediately to explain the supported import path.

throw new Error(
"Importing '@jest/globals' is not supported in Harness tests. Import from 'react-native-harness' instead."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';
import fs from 'node:fs';
import path from 'node:path';
import { Config as HarnessConfig } from '@react-native-harness/config';
import { getHarnessManifestPath } from './paths';
import { getHarnessManifestPath } from './paths.js';

const getManifestContent = (harnessConfig: HarnessConfig): string => {
return `global.RN_HARNESS = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CacheStore, MetroCache } from 'metro-cache';
import type { MixedOutput, TransformResult } from 'metro';
import fs from 'node:fs';
import type { CacheStoresConfigT } from 'metro-config';
import { getHarnessMetroCachePath } from './paths';
import { CacheStore, MetroCache } from 'metro-cache';
import type { MixedOutput, TransformResult } from 'metro';
import { getHarnessMetroCachePath } from './paths.js';

export const getHarnessCacheStores = (): ((
metroCache: MetroCache
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { HarnessResolver, MetroResolver } from './types';
import type { HarnessResolver, MetroResolver } from './types.js';

export const createHarnessResolver = (
resolvers: HarnessResolver[]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import type { MetroConfig } from '@react-native/metro-config';
import type { Config as HarnessConfig } from '@react-native-harness/config';
import path from 'node:path';
import { createHarnessResolver } from './composite-resolver';
import { createTsConfigResolver } from './tsconfig-resolver';
import type { HarnessResolver, MetroResolver } from './types';
import { createHarnessResolver } from './composite-resolver.js';
import { createTsConfigResolver } from './tsconfig-resolver.js';
import type { HarnessResolver, MetroResolver } from './types.js';

// Safely resolves a path and strips its extension
const getExtensionlessAbsolutePath = (basePath: string, relativePath = ''): string => {
const require = createRequire(import.meta.url);

const getExtensionlessAbsolutePath = (
basePath: string,
relativePath = ''
): string => {
const fullPath = path.resolve(basePath, relativePath);
const parsed = path.parse(fullPath);
return path.join(parsed.dir, parsed.name);
}
};

export const createHarnessEntryPointResolver = (harnessConfig: HarnessConfig): HarnessResolver => {
export const createHarnessEntryPointResolver = (
harnessConfig: HarnessConfig
): HarnessResolver => {
const rootPath = path.resolve(process.cwd());
const expectedEntryPoint = getExtensionlessAbsolutePath(rootPath, harnessConfig.entryPoint);
const resolvedHarnessPath = require.resolve('@react-native-harness/runtime/entry-point');
const expectedEntryPoint = getExtensionlessAbsolutePath(
rootPath,
harnessConfig.entryPoint
);
const resolvedHarnessPath = require.resolve(
'@react-native-harness/runtime/entry-point'
);

return (context, moduleName, _platform) => {
// 1. Resolve the origin path of the file making the import
return (context, moduleName, platform) => {
void platform;
const currentOrigin = path.resolve(context.originModulePath);

// Fast Fail: If the import isn't happening from the root directory, skip it immediately
if (currentOrigin !== rootPath) {
return null;
}

// 2. Resolve the module being imported and strip its extension
// This safely normalizes './index', './index.js', 'index.js', etc.
const requestedModule = getExtensionlessAbsolutePath(currentOrigin, moduleName);
const requestedModule = getExtensionlessAbsolutePath(
currentOrigin,
moduleName
);

// 3. String comparison
if (requestedModule === expectedEntryPoint) {
return {
type: 'sourceFile',
Expand All @@ -43,12 +54,12 @@ export const createHarnessEntryPointResolver = (harnessConfig: HarnessConfig): H
};

export const createJestGlobalsResolver = (): HarnessResolver => {
return (_context, moduleName, _platform) => {
// Intercept @jest/globals imports and redirect to mock module
return (_context, moduleName, platform) => {
void platform;
if (moduleName === '@jest/globals') {
return {
type: 'sourceFile',
filePath: require.resolve('../jest-globals-mock'),
filePath: require.resolve('../jest-globals-mock.js'),
};
}

Expand All @@ -64,7 +75,8 @@ export const createJsxRuntimeResolver = (): HarnessResolver => {
'@react-native-harness/runtime/jsx-dev-runtime'
);

return (_context, moduleName, _platform) => {
return (_context, moduleName, platform) => {
void platform;
if (moduleName === '@react-native-harness/runtime/jsx-runtime') {
return {
type: 'sourceFile',
Expand Down
Loading
Loading