Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .nx/version-plans/capability-aware-ios-startup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@react-native-harness/tools': patch
'@react-native-harness/platform-apple': patch
---

Harness now adapts iOS permission-agent startup to the host's available memory
and CPU capacity, preserving build-first startup on constrained machines while
overlapping safe preparation work on capable hosts.
73 changes: 40 additions & 33 deletions packages/bundler-metro/src/__tests__/metro-block-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,46 @@ const withBlockList = (
blockList: NonNullable<MetroConfig['resolver']>['blockList']
): MetroConfig => ({ resolver: { blockList } }) as MetroConfig;

const HARNESS_CACHE_ROOT = '/p/.harness/cache';

const getBlockList = (
blockList: NonNullable<MetroConfig['resolver']>['blockList']
) => getHarnessBlockList(withBlockList(blockList), HARNESS_CACHE_ROOT);

describe('getHarnessBlockList', () => {
describe('harness-owned exclusions', () => {
it('excludes the cache harness creates under .harness', () => {
const { blockList } = getHarnessBlockList(withBlockList(undefined));
const { blockList } = getBlockList(undefined);

expect(blockList.test('/p/.harness/cache/metro/ab/cdef')).toBe(true);
expect(blockList.test('/p/.harness/cache/metro-file-map/map.v1')).toBe(
true
);
expect(blockList.test('\\p\\.harness\\cache\\metro\\ab\\cdef')).toBe(
true
);
});

it('only excludes the canonical cache root', () => {
const { blockList } = getBlockList(undefined);

expect(blockList.test('/other/.harness/cache/metro/ab/cdef')).toBe(false);
expect(blockList.test('/p/.harness/cache-backup/metro/ab/cdef')).toBe(
false
);
});

it('keeps the harness manifest crawlable', () => {
// The manifest is injected via `serializer.getPolyfills`, so a module
// missing from the file map fails with `Failed to get the SHA-1`.
const { blockList } = getHarnessBlockList(withBlockList(undefined));
const { blockList } = getBlockList(undefined);

expect(blockList.test(getHarnessManifestPath('/p'))).toBe(false);
expect(blockList.test('/p/.harness/manifest.js')).toBe(false);
});

it('adds nothing else of its own', () => {
const { blockList } = getHarnessBlockList(withBlockList(undefined));
const { blockList } = getBlockList(undefined);

for (const path of [
'/p/.nx/cache/a.js',
Expand All @@ -60,47 +78,40 @@ describe('getHarnessBlockList', () => {

describe('inheriting the project blockList', () => {
it('honours a plain project pattern', () => {
const { blockList, dropped } = getHarnessBlockList(
withBlockList(/[/\\]fixtures[/\\]/)
);
const { blockList, dropped } = getBlockList(/[/\\]fixtures[/\\]/);

expect(dropped).toEqual([]);
expect(blockList.test('/p/src/fixtures/big.json')).toBe(true);
expect(blockList.test('/p/src/app.tsx')).toBe(false);
});

it('honours an array of project patterns', () => {
const { blockList, dropped } = getHarnessBlockList(
withBlockList([/[/\\]fixtures[/\\]/, /\.snap$/])
);
const { blockList, dropped } = getBlockList([
/[/\\]fixtures[/\\]/,
/\.snap$/,
]);

expect(dropped).toEqual([]);
expect(blockList.test('/p/src/fixtures/big.json')).toBe(true);
expect(blockList.test('/p/src/a.snap')).toBe(true);
});

it('honours an anchored project pattern', () => {
const { blockList } = getHarnessBlockList(
withBlockList(/^\/p\/vendor\//)
);
const { blockList } = getBlockList(/^\/p\/vendor\//);

expect(blockList.test('/p/vendor/lib.js')).toBe(true);
expect(blockList.test('/other/p/vendor/lib.js')).toBe(false);
});

it('preserves group numbering so backreferences still work', () => {
const { blockList } = getHarnessBlockList(
withBlockList(/([/\\])dup\1/)
);
const { blockList } = getBlockList(/([/\\])dup\1/);

expect(blockList.test('/p/dup/dup')).toBe(true);
expect(blockList.test('/p/dup\\dup')).toBe(false);
});

it('adopts project flags so a case-insensitive pattern keeps working', () => {
const { blockList, dropped } = getHarnessBlockList(
withBlockList(/[/\\]FIXTURES[/\\]/i)
);
const { blockList, dropped } = getBlockList(/[/\\]FIXTURES[/\\]/i);

expect(dropped).toEqual([]);
expect(blockList.flags).toBe('i');
Expand All @@ -110,9 +121,11 @@ describe('getHarnessBlockList', () => {
it('drops minority-flag patterns instead of letting Metro throw', () => {
// Metro's array handling throws when combining mismatched flags.
const minority = /[/\\]other[/\\]/i;
const { blockList, dropped } = getHarnessBlockList(
withBlockList([/[/\\]a[/\\]/, /[/\\]b[/\\]/, minority])
);
const { blockList, dropped } = getBlockList([
/[/\\]a[/\\]/,
/[/\\]b[/\\]/,
minority,
]);

expect(dropped).toEqual([
{ pattern: minority, reason: 'incompatible-flags' },
Expand All @@ -123,9 +136,7 @@ describe('getHarnessBlockList', () => {
});

it('is stateless across calls when given a global pattern', () => {
const { blockList } = getHarnessBlockList(
withBlockList(/[/\\]fixtures[/\\]/g)
);
const { blockList } = getBlockList(/[/\\]fixtures[/\\]/g);

expect(blockList.test('/p/src/fixtures/a.json')).toBe(true);
expect(blockList.test('/p/src/fixtures/a.json')).toBe(true);
Expand All @@ -134,9 +145,7 @@ describe('getHarnessBlockList', () => {

describe('carving __tests__ out of the inherited blockList', () => {
it("keeps tests crawlable under Metro's stock blockList", () => {
const { blockList, dropped } = getHarnessBlockList(
withBlockList(exclusionList())
);
const { blockList, dropped } = getBlockList(exclusionList());

expect(dropped).toEqual([]);
expect(blockList.test('/p/src/__tests__/smoke.harness.ts')).toBe(false);
Expand All @@ -145,8 +154,8 @@ describe('getHarnessBlockList', () => {
it("keeps a project's own exclusions while still crawling tests", () => {
// exclusionList fuses the project's patterns and the __tests__ rule into
// a single alternation, so the two cannot be separated by inspection.
const { blockList, dropped } = getHarnessBlockList(
withBlockList(exclusionList([/ios\/build\/.*/]))
const { blockList, dropped } = getBlockList(
exclusionList([/ios\/build\/.*/])
);

expect(dropped).toEqual([]);
Expand All @@ -156,9 +165,7 @@ describe('getHarnessBlockList', () => {
});

it('keeps tests crawlable even inside an otherwise excluded directory', () => {
const { blockList } = getHarnessBlockList(
withBlockList(/[/\\]generated[/\\]/)
);
const { blockList } = getBlockList(/[/\\]generated[/\\]/);

expect(blockList.test('/p/generated/a.js')).toBe(true);
expect(blockList.test('/p/generated/__tests__/a.harness.ts')).toBe(false);
Expand All @@ -183,7 +190,7 @@ describe('getHarnessBlockList', () => {
];

for (const pattern of patterns) {
const { blockList } = getHarnessBlockList(withBlockList(pattern));
const { blockList } = getBlockList(pattern);

for (const path of paths) {
const expected =
Expand Down
26 changes: 10 additions & 16 deletions packages/bundler-metro/src/metro-block-list.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import type { MetroConfig } from 'metro-config';
import { escapeRegExp } from '@react-native-harness/tools';

type BlockList = NonNullable<MetroConfig['resolver']>['blockList'];

/**
* The only directory harness excludes on its own behalf: the cache it creates
* and maintains under `.harness`, which Metro never needs to resolve or read.
*
* Scoped to `.harness/cache` rather than all of `.harness` on purpose --
* `.harness/manifest.js` is served as a polyfill via
* `serializer.getPolyfills`, and a module that harness injects into the graph
* but that is missing from the file map fails the build with
* `Failed to get the SHA-1 for: <path>`.
*
* Nothing else is added here. Excluding third-party directories (build output,
* tool caches, native dependency trees) is the project's call, expressed
* through `resolver.blockList` in its own `metro.config.js`, which harness
* inherits below.
* Converts the canonical cache root into a cross-platform regular-expression
* source without duplicating knowledge of the cache directory layout.
*/
const HARNESS_OWNED_EXCLUSIONS = /[/\\]\.harness[/\\]cache(?:[/\\]|$)/;
const getHarnessCacheRootPatternSource = (harnessCacheRoot: string): string =>
escapeRegExp(harnessCacheRoot).replace(/\\\\|\//g, '[/\\\\]');

/**
* Paths harness must be able to crawl, carved out of whatever the project
Expand Down Expand Up @@ -84,7 +75,8 @@ export type BlockListDrop = {
* conflicts by dropping a pattern instead of failing the run.
*/
export const getHarnessBlockList = (
metroConfig: MetroConfig
metroConfig: MetroConfig,
harnessCacheRoot: string
): { blockList: RegExp; dropped: BlockListDrop[] } => {
const dropped: BlockListDrop[] = [];
const userPatterns = toPatternArray(metroConfig.resolver?.blockList);
Expand All @@ -100,7 +92,9 @@ export const getHarnessBlockList = (
const flags =
[...flagCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? '';

const sources = [HARNESS_OWNED_EXCLUSIONS.source];
const sources = [
`^${getHarnessCacheRootPatternSource(harnessCacheRoot)}(?:[/\\\\]|$)`,
];
for (const pattern of userPatterns) {
if (stripStatefulFlags(pattern.flags) === flags) {
sources.push(carveOutProtectedPaths(pattern.source));
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler-metro/src/withRnHarness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const withRnHarness = <T extends MetroConfig>(
getHarnessBabelTransformerPath(metroConfig);

const { blockList: harnessBlockList, dropped: droppedBlockListPatterns } =
getHarnessBlockList(metroConfig);
getHarnessBlockList(metroConfig, harnessCache.paths.root);

for (const { pattern } of droppedBlockListPatterns) {
metroBlockListLogger.warn(
Expand Down
Loading
Loading