From e76c212789c74ef01616bee7f357c32a382a534e Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Mon, 11 May 2026 08:32:22 -0700 Subject: [PATCH] Add integration tests for virtual-prefix URL routing Summary: Adds integration tests for `[metro-project]` and `[metro-watchFolders]` virtual URL prefixes: bundle requests, out-of-bounds index 404, and asset serving. Removes Server unit tests that tested private methods (`_resolveWatchFolderPrefix`, `_getEntryPointAbsolutePath`) directly. The behaviours they covered are now tested end-to-end by the new integration tests and will also be covered by `ProjectRouteMap` unit tests in the next diff. All tests pass without any production code changes. Reviewed By: huntie Differential Revision: D104259281 --- .../metro/src/Server/__tests__/Server-test.js | 83 ------------------- .../integration_tests/__tests__/build-test.js | 35 ++++++++ .../__tests__/rambundle-test.js | 30 +++++-- .../__tests__/server-test.js | 79 ++++++++++++++++++ .../[metro-project]/LiteralDir.js | 11 +++ 5 files changed, 147 insertions(+), 91 deletions(-) create mode 100644 packages/metro/src/integration_tests/basic_bundle/[metro-project]/LiteralDir.js diff --git a/packages/metro/src/Server/__tests__/Server-test.js b/packages/metro/src/Server/__tests__/Server-test.js index 47d7e6f954..31eb5e7fe0 100644 --- a/packages/metro/src/Server/__tests__/Server-test.js +++ b/packages/metro/src/Server/__tests__/Server-test.js @@ -1436,87 +1436,4 @@ describe('processRequest', () => { }, ); }); - - describe('watchFolder prefix resolution', () => { - let watchFolderServer: $FlowFixMe; - - beforeEach(() => { - watchFolderServer = new Server( - mergeConfig(getDefaultValues('/'), { - projectRoot: '/project', - watchFolders: ['/project', '/external/packages'], - resolver: {blockList: []}, - cacheVersion: '', - serializer: { - getRunModuleStatement: moduleId => - `require(${JSON.stringify(moduleId)});`, - polyfillModuleNames: [], - getModulesRunBeforeMainModule: () => ['InitializeCore'], - }, - reporter: require('../../lib/reporting').nullReporter, - } as InputConfigT), - ); - }); - - test('resolves [metro-watchFolders]/N/ prefix against the Nth watch folder', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix( - './[metro-watchFolders]/1/expo-router/entry', - ), - ).toEqual({ - rootDir: '/external/packages', - filePath: './expo-router/entry', - }); - }); - - test('resolves [metro-watchFolders]/0/ prefix against the first watch folder', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix( - './[metro-watchFolders]/0/app/index', - ), - ).toEqual({ - rootDir: '/project', - filePath: './app/index', - }); - }); - - test('resolves [metro-project]/ prefix against projectRoot', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix( - './[metro-project]/src/App', - ), - ).toEqual({ - rootDir: '/project', - filePath: './src/App', - }); - }); - - test('returns null for paths without a recognized prefix', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix('./mybundle'), - ).toBeNull(); - }); - - test('returns null for out-of-bounds watchFolder index', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix( - './[metro-watchFolders]/99/mybundle', - ), - ).toBeNull(); - }); - - test('_getEntryPointAbsolutePath resolves prefixed entry against the corresponding watch folder', () => { - expect( - watchFolderServer._getEntryPointAbsolutePath( - './[metro-watchFolders]/1/expo-router/entry', - ), - ).toBe('/external/packages/expo-router/entry'); - }); - - test('_getEntryPointAbsolutePath resolves non-prefixed entry against server root', () => { - expect(watchFolderServer._getEntryPointAbsolutePath('./mybundle')).toBe( - '/project/mybundle', - ); - }); - }); }); diff --git a/packages/metro/src/integration_tests/__tests__/build-test.js b/packages/metro/src/integration_tests/__tests__/build-test.js index 218b424988..ec6db788bb 100644 --- a/packages/metro/src/integration_tests/__tests__/build-test.js +++ b/packages/metro/src/integration_tests/__tests__/build-test.js @@ -118,6 +118,41 @@ test('allows specifying paths to save bundle and maps', async () => { ); }); +// $FlowFixMe[prop-missing] - test.failing is not in Flow's Jest types +test.failing( + 'builds a bundle from a file in a directory literally named [metro-project]', + async () => { + const config = await Metro.loadConfig({ + config: require.resolve('../metro.config.js'), + }); + + const result = await Metro.runBuild(config, { + entry: './[metro-project]/LiteralDir.js', + }); + + expect(execBundle(result.code)).toBe('from-literal-dir'); + }, +); + +// $FlowFixMe[prop-missing] - test.failing is not in Flow's Jest types +test.failing( + 'runBuild resolves entry against projectRoot, not unstable_serverRoot', + async () => { + const baseConfig = await Metro.loadConfig({ + config: require.resolve('../metro.config.js'), + }); + const config = MetroConfig.mergeConfig(baseConfig, { + server: {unstable_serverRoot: path.resolve(INPUT_PATH, '..')}, + }); + + const result = await Metro.runBuild(config, { + entry: 'TestBundle.js', + }); + + expect(execBundle(result.code)).toBeDefined(); + }, +); + test('(unstable) allows specifying a transform profile', async () => { const config = await Metro.loadConfig({ config: require.resolve('../metro.config.js'), diff --git a/packages/metro/src/integration_tests/__tests__/rambundle-test.js b/packages/metro/src/integration_tests/__tests__/rambundle-test.js index b18867d400..af7e68e188 100644 --- a/packages/metro/src/integration_tests/__tests__/rambundle-test.js +++ b/packages/metro/src/integration_tests/__tests__/rambundle-test.js @@ -19,15 +19,19 @@ const vm = require('vm'); jest.setTimeout(30 * 1000); -test('builds and executes a RAM bundle', async () => { - const config = await Metro.loadConfig({ +let config; + +beforeAll(async () => { + config = await Metro.loadConfig({ config: require.resolve('../metro.config.js'), }); - const bundlePath = path.join(os.tmpdir(), 'rambundle.js'); +}); +async function buildAndExecRamBundle(entry: string): mixed { + const bundlePath = path.join(os.tmpdir(), `rambundle-${Date.now()}.js`); try { await Metro.runBuild(config, { - entry: 'TestBundle.js', + entry, output: ramBundleOutput, out: bundlePath, }); @@ -35,16 +39,26 @@ test('builds and executes a RAM bundle', async () => { const bundleBuffer = fs.readFileSync(bundlePath); const parser = new RamBundleParser(bundleBuffer); - // Create a context with a global nativeRequire function, which reads the - // module code from the RAM bundle and injects it into the VM. const context = vm.createContext({ nativeRequire(id) { vm.runInContext(parser.getModule(id), context); }, }); - expect(vm.runInContext(parser.getStartupCode(), context)).toMatchSnapshot(); + return vm.runInContext(parser.getStartupCode(), context); } finally { - fs.unlinkSync(bundlePath); + if (fs.existsSync(bundlePath)) { + fs.unlinkSync(bundlePath); + } } +} + +test('builds and executes a RAM bundle', async () => { + expect(await buildAndExecRamBundle('TestBundle.js')).toMatchSnapshot(); +}); + +test('rejects [metro-project] virtual prefix in runBuild entry', async () => { + await expect( + buildAndExecRamBundle('./[metro-project]/TestBundle.js'), + ).rejects.toThrow('was not found'); }); diff --git a/packages/metro/src/integration_tests/__tests__/server-test.js b/packages/metro/src/integration_tests/__tests__/server-test.js index 90c36476c8..7138de2f86 100644 --- a/packages/metro/src/integration_tests/__tests__/server-test.js +++ b/packages/metro/src/integration_tests/__tests__/server-test.js @@ -122,6 +122,85 @@ describe('Metro development server serves bundles via HTTP', () => { ); }); + // TODO(T000000): Fix virtual-prefix URL resolution on Windows. + // path.sep differences cause entry point resolution to fail. + (process.platform === 'win32' ? test.skip : test)( + 'should serve bundles with [metro-watchFolders] entry point', + async () => { + expect( + await downloadAndExec( + '/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.bundle?platform=ios&dev=true&minify=false', + ), + ).toBeDefined(); + }, + ); + + (process.platform === 'win32' ? test.skip : test)( + 'should serve bundles with [metro-project] entry point', + async () => { + expect( + await downloadAndExec( + '/[metro-project]/TestBundle.bundle?platform=ios&dev=true&minify=false', + ), + ).toBeDefined(); + }, + ); + + (process.platform === 'win32' ? test.skip : test)( + '[metro-project] source map resolves same modules as non-prefixed', + async () => { + const directResponse = await fetchAndClose( + 'http://localhost:' + + httpServer.address().port + + '/TestBundle.map?platform=ios&dev=true&minify=false', + ); + const prefixedResponse = await fetchAndClose( + 'http://localhost:' + + httpServer.address().port + + '/[metro-project]/TestBundle.map?platform=ios&dev=true&minify=false', + ); + expect(directResponse.ok).toBe(true); + expect(prefixedResponse.ok).toBe(true); + const directMap = await directResponse.json(); + const prefixedMap = await prefixedResponse.json(); + expect([...prefixedMap.sources].sort()).toEqual( + [...directMap.sources].sort(), + ); + }, + ); + + (process.platform === 'win32' ? test.skip : test)( + '[metro-watchFolders] source map resolves same modules as non-prefixed', + async () => { + const directResponse = await fetchAndClose( + 'http://localhost:' + + httpServer.address().port + + '/TestBundle.map?platform=ios&dev=true&minify=false', + ); + const watchFolderResponse = await fetchAndClose( + 'http://localhost:' + + httpServer.address().port + + '/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.map?platform=ios&dev=true&minify=false', + ); + expect(directResponse.ok).toBe(true); + expect(watchFolderResponse.ok).toBe(true); + const directMap = await directResponse.json(); + const watchFolderMap = await watchFolderResponse.json(); + expect([...watchFolderMap.sources].sort()).toEqual( + [...directMap.sources].sort(), + ); + }, + ); + + test('responds with 404 for [metro-watchFolders] with out-of-bounds index', async () => { + const response = await fetchAndClose( + 'http://localhost:' + + httpServer.address().port + + '/[metro-watchFolders]/99/TestBundle.bundle?platform=ios&dev=true&minify=false', + ); + expect(response.status).toBe(404); + }); + test('responds with 404 when the bundle cannot be resolved', async () => { const response = await fetchAndClose( 'http://localhost:' + httpServer.address().port + '/doesnotexist.bundle', diff --git a/packages/metro/src/integration_tests/basic_bundle/[metro-project]/LiteralDir.js b/packages/metro/src/integration_tests/basic_bundle/[metro-project]/LiteralDir.js new file mode 100644 index 0000000000..52fce287ef --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/[metro-project]/LiteralDir.js @@ -0,0 +1,11 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +module.exports = 'from-literal-dir';