Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1787953392799.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: patch
---

The harness now runs on a Windows host and recognizes React Native Windows as a device platform: ESM (`rn-harness.config.mjs`) configs load correctly when the harness process runs on Windows, and an app reporting `Platform.OS === 'windows'` completes the bridge handshake instead of failing with "Unsupported platform".
2 changes: 1 addition & 1 deletion packages/bridge/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export type {
} from './shared/bundler.js';

export type DeviceDescriptor = {
platform: 'ios' | 'android' | 'vega' | 'web';
platform: 'ios' | 'android' | 'vega' | 'web' | 'windows';
manufacturer: string;
model: string;
osVersion: string;
Expand Down
82 changes: 82 additions & 0 deletions packages/config/src/__tests__/reader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { getConfig } from '../reader.js';

const CONFIG_BODY = {
entryPoint: './index.js',
appRegistryComponentName: 'App',
runners: [
{
name: 'test-runner',
config: {},
runner: 'test-runner',
platformId: 'test-platform',
},
],
};

let projectDir: string;

beforeEach(() => {
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-harness-reader-'));
});

afterEach(() => {
fs.rmSync(projectDir, { recursive: true, force: true });
});

describe('getConfig', () => {
it('loads an ESM (.mjs) config via a file:// URL', async () => {
// A bare absolute path passed to dynamic import() is rejected on Windows
// (ERR_UNSUPPORTED_ESM_URL_SCHEME because `C:` reads as a URL scheme); the
// reader must convert it with pathToFileURL first. This exercises that path
// on every OS and regression-guards it on Windows.
fs.writeFileSync(
path.join(projectDir, 'rn-harness.config.mjs'),
`export default ${JSON.stringify(CONFIG_BODY)};\n`
);

const { config, projectRoot } = await getConfig(projectDir);

expect(config.entryPoint).toBe('./index.js');
expect(config.runners).toHaveLength(1);
expect(projectRoot).toBe(projectDir);
});

it('loads a CommonJS (.js) config', async () => {
fs.writeFileSync(
path.join(projectDir, 'rn-harness.config.js'),
`module.exports = ${JSON.stringify(CONFIG_BODY)};\n`
);

const { config } = await getConfig(projectDir);

expect(config.appRegistryComponentName).toBe('App');
});

it('loads a JSON config', async () => {
fs.writeFileSync(
path.join(projectDir, 'rn-harness.config.json'),
JSON.stringify(CONFIG_BODY)
);

const { config } = await getConfig(projectDir);

expect(config.entryPoint).toBe('./index.js');
});

it('walks up to a parent directory to find the config', async () => {
fs.writeFileSync(
path.join(projectDir, 'rn-harness.config.mjs'),
`export default ${JSON.stringify(CONFIG_BODY)};\n`
);
const nested = path.join(projectDir, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });

const { projectRoot } = await getConfig(nested);

expect(projectRoot).toBe(projectDir);
});
});
7 changes: 6 additions & 1 deletion packages/config/src/reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from './errors.js';
import path from 'node:path';
import fs from 'node:fs';
import { pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
import { ZodError } from 'zod';

Expand All @@ -28,7 +29,11 @@ const importUp = async (

try {
if (ext === '.mjs') {
rawConfig = await import(filePathWithExt).then(
// A dynamic import() of an absolute path only accepts a file:// URL.
// On POSIX the bare path happens to work; on Windows it is read as a
// URL and `C:` is rejected as an unknown scheme
// (ERR_UNSUPPORTED_ESM_URL_SCHEME). pathToFileURL normalizes both.
rawConfig = await import(pathToFileURL(filePathWithExt).href).then(
(module) => module.default
);
} else {
Expand Down
95 changes: 95 additions & 0 deletions packages/runtime/src/client/getDeviceDescriptor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getDeviceDescriptor } from './getDeviceDescriptor.js';

const mocks = vi.hoisted(() => ({
Platform: {
OS: 'ios' as string,
constants: {} as Record<string, unknown>,
},
}));

vi.mock('react-native', () => ({
Platform: mocks.Platform,
}));

beforeEach(() => {
mocks.Platform.OS = 'ios';
mocks.Platform.constants = {};
});

describe('getDeviceDescriptor', () => {
it('describes an iOS device', () => {
mocks.Platform.OS = 'ios';
mocks.Platform.constants = { osVersion: '17.4' };

expect(getDeviceDescriptor()).toEqual({
platform: 'ios',
manufacturer: 'Apple',
model: 'Unknown',
osVersion: '17.4',
});
});

it('describes an Android device', () => {
mocks.Platform.OS = 'android';
mocks.Platform.constants = {
Manufacturer: 'Google',
Model: 'Pixel 8',
Release: '14',
};

expect(getDeviceDescriptor()).toEqual({
platform: 'android',
manufacturer: 'Google',
model: 'Pixel 8',
osVersion: '14',
});
});

it('describes web', () => {
mocks.Platform.OS = 'web';

expect(getDeviceDescriptor()).toEqual({
platform: 'web',
manufacturer: '',
model: '',
osVersion: '',
});
});

it('maps the kepler OS to the vega platform', () => {
mocks.Platform.OS = 'kepler';

expect(getDeviceDescriptor()).toEqual({
platform: 'vega',
manufacturer: '',
model: '',
osVersion: '',
});
});

it('describes a Windows device', () => {
mocks.Platform.OS = 'windows';
mocks.Platform.constants = { osVersion: 10 };

expect(getDeviceDescriptor()).toEqual({
platform: 'windows',
manufacturer: '',
model: '',
osVersion: '10',
});
});

it('tolerates a Windows device without an osVersion constant', () => {
mocks.Platform.OS = 'windows';
mocks.Platform.constants = {};

expect(getDeviceDescriptor().osVersion).toBe('');
});

it('throws for an unknown platform', () => {
mocks.Platform.OS = 'tizen';

expect(() => getDeviceDescriptor()).toThrow('Unsupported platform');
});
});
11 changes: 10 additions & 1 deletion packages/runtime/src/client/getDeviceDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const getPlatform = (): Platform | PlatformKeplerStatic => {
};

export type DeviceDescriptor = {
platform: 'ios' | 'android' | 'vega' | 'web';
platform: 'ios' | 'android' | 'vega' | 'web' | 'windows';
manufacturer: string;
model: string;
osVersion: string;
Expand Down Expand Up @@ -56,5 +56,14 @@ export const getDeviceDescriptor = (): DeviceDescriptor => {
};
}

if (platform.OS === 'windows') {
return {
platform: 'windows',
manufacturer: '',
model: '',
osVersion: String(platform.constants?.osVersion ?? ''),
};
}

throw new Error('Unsupported platform');
};