From c0a3b43e21706cebb8029586235b38c3f9f684bd Mon Sep 17 00:00:00 2001 From: zhanglangning Date: Tue, 11 Aug 2026 16:26:59 +0800 Subject: [PATCH 1/2] Propagate transformer initialization errors --- packages/metro/src/Bundler.js | 5 ++ packages/metro/src/__tests__/Bundler-test.js | 87 ++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 packages/metro/src/__tests__/Bundler-test.js diff --git a/packages/metro/src/Bundler.js b/packages/metro/src/Bundler.js index 947c9b969f..e344e93729 100644 --- a/packages/metro/src/Bundler.js +++ b/packages/metro/src/Bundler.js @@ -46,7 +46,12 @@ export default class Bundler { type: 'transformer_load_failed', error, }); + throw error; }); + + // Observe initialization failures immediately so callers can await the + // original promise later without triggering an unhandled rejection. + this._initializedPromise.catch(() => {}); } getWatcher(): EventEmitter { diff --git a/packages/metro/src/__tests__/Bundler-test.js b/packages/metro/src/__tests__/Bundler-test.js new file mode 100644 index 0000000000..bf9c31ca77 --- /dev/null +++ b/packages/metro/src/__tests__/Bundler-test.js @@ -0,0 +1,87 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +'use strict'; + +jest.mock('../DeltaBundler/Transformer'); +jest.mock('../node-haste/DependencyGraph'); + +const Bundler = require('../Bundler').default; +const Transformer = require('../DeltaBundler/Transformer').default; +const DependencyGraph = require('../node-haste/DependencyGraph').default; +const {getDefaultValues} = require('metro-config').getDefaultConfig; + +describe('Bundler', () => { + let config; + let reporter; + + beforeEach(() => { + reporter = {update: jest.fn()}; + config = {...getDefaultValues('/'), reporter}; + + DependencyGraph.mockImplementation(() => ({ + ready: jest.fn().mockResolvedValue(), + })); + + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test.each([ + ['ready', bundler => bundler.ready()], + ['transformFile', bundler => bundler.transformFile('/entry.js', {})], + ])( + 'propagates Transformer initialization errors from %s', + async (_method, invoke) => { + const error = new Error('Transformer initialization failed'); + Transformer.mockImplementation(() => { + throw error; + }); + + const bundler = new Bundler(config); + + await expect(invoke(bundler)).rejects.toBe(error); + expect(reporter.update).toHaveBeenCalledWith({ + type: 'transformer_load_failed', + error, + }); + }, + ); + + test('does not emit an unhandled rejection before ready is called', async () => { + jest.useRealTimers(); + + const error = new Error('Transformer initialization failed'); + const unhandledRejections = []; + const onUnhandledRejection = reason => { + unhandledRejections.push(reason); + }; + + Transformer.mockImplementation(() => { + throw error; + }); + process.on('unhandledRejection', onUnhandledRejection); + + try { + const bundler = new Bundler(config); + + await new Promise(resolve => setImmediate(resolve)); + + await expect(bundler.ready()).rejects.toBe(error); + expect(unhandledRejections).toEqual([]); + } finally { + process.off('unhandledRejection', onUnhandledRejection); + jest.useFakeTimers(); + } + }); +}); From 7ed27c8c542338eec4b12e5ba84e5ec18159fbf0 Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:02:44 +0100 Subject: [PATCH 2/2] Type Bundler-test with Flow and ESM imports Adds `@flow strict-local` to the new `Bundler-test.js` and switches its `require` calls to ESM imports, matching neighboring tests such as `HmrServer-test.js`. Changelog: [Internal] --- packages/metro/src/__tests__/Bundler-test.js | 40 +++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/metro/src/__tests__/Bundler-test.js b/packages/metro/src/__tests__/Bundler-test.js index bf9c31ca77..8515b01c5f 100644 --- a/packages/metro/src/__tests__/Bundler-test.js +++ b/packages/metro/src/__tests__/Bundler-test.js @@ -4,29 +4,37 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format * @oncall react_native */ -'use strict'; +import type {ConfigT} from 'metro-config'; + +import Bundler from '../Bundler'; +import Transformer from '../DeltaBundler/Transformer'; +import DependencyGraph from '../node-haste/DependencyGraph'; +import {getDefaultConfig} from 'metro-config'; jest.mock('../DeltaBundler/Transformer'); jest.mock('../node-haste/DependencyGraph'); -const Bundler = require('../Bundler').default; -const Transformer = require('../DeltaBundler/Transformer').default; -const DependencyGraph = require('../node-haste/DependencyGraph').default; -const {getDefaultValues} = require('metro-config').getDefaultConfig; +type ClassMock = JestMockFn, unknown>; + +// $FlowFixMe[incompatible-type] Jest automocks the default export +const MockTransformer: ClassMock = Transformer; +// $FlowFixMe[incompatible-type] Jest automocks the default export +const MockDependencyGraph: ClassMock = DependencyGraph; describe('Bundler', () => { - let config; + let config: ConfigT; let reporter; beforeEach(() => { reporter = {update: jest.fn()}; - config = {...getDefaultValues('/'), reporter}; + config = {...getDefaultConfig.getDefaultValues('/'), reporter}; - DependencyGraph.mockImplementation(() => ({ + MockDependencyGraph.mockImplementation(() => ({ ready: jest.fn().mockResolvedValue(), })); @@ -38,13 +46,17 @@ describe('Bundler', () => { }); test.each([ - ['ready', bundler => bundler.ready()], - ['transformFile', bundler => bundler.transformFile('/entry.js', {})], + ['ready', (bundler: Bundler) => bundler.ready()], + [ + 'transformFile', + // $FlowFixMe[incompatible-type] Transform options are unused before initialization fails + (bundler: Bundler) => bundler.transformFile('/entry.js', {}), + ], ])( 'propagates Transformer initialization errors from %s', async (_method, invoke) => { const error = new Error('Transformer initialization failed'); - Transformer.mockImplementation(() => { + MockTransformer.mockImplementation(() => { throw error; }); @@ -62,12 +74,12 @@ describe('Bundler', () => { jest.useRealTimers(); const error = new Error('Transformer initialization failed'); - const unhandledRejections = []; - const onUnhandledRejection = reason => { + const unhandledRejections: Array = []; + const onUnhandledRejection = (reason: unknown) => { unhandledRejections.push(reason); }; - Transformer.mockImplementation(() => { + MockTransformer.mockImplementation(() => { throw error; }); process.on('unhandledRejection', onUnhandledRejection);