diff --git a/packages/metro/src/Server/__tests__/Server-test.js b/packages/metro/src/Server/__tests__/Server-test.js index 82e66ac844..b1e8c5e46c 100644 --- a/packages/metro/src/Server/__tests__/Server-test.js +++ b/packages/metro/src/Server/__tests__/Server-test.js @@ -1614,67 +1614,6 @@ describe('processRequest', () => { ); }); - test('resolves [metro-watchFolders]/N/ prefix against the Nth watch folder', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix( - './[metro-watchFolders]/1/expo-router/entry', - ), - ).toEqual({ - rootDir: p('/external/packages'), - filePath: p('./expo-router/entry'), - }); - }); - - test('resolves [metro-watchFolders]/0/ prefix against the first watch folder', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix( - './[metro-watchFolders]/0/app/index', - ), - ).toEqual({ - rootDir: p('/project'), - filePath: p('./app/index'), - }); - }); - - test('resolves [metro-project]/ prefix against projectRoot', () => { - expect( - watchFolderServer._resolveWatchFolderPrefix( - './[metro-project]/src/App', - ), - ).toEqual({ - rootDir: p('/project'), - filePath: p('./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(p('/external/packages/expo-router/entry')); - }); - - test('_getEntryPointAbsolutePath resolves non-prefixed entry against server root', () => { - expect(watchFolderServer._getEntryPointAbsolutePath('./mybundle')).toBe( - p('/project/mybundle'), - ); - }); - test.each([ [p('/project/imgs/a.png')], [p('/project/nested/deep/b.png')], diff --git a/packages/metro/src/integration_tests/__tests__/build-test.js b/packages/metro/src/integration_tests/__tests__/build-test.js index 0014c6cd85..8b90289240 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 a18230599f..3af7dbaee8 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('node: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 39b5a5169a..bb744e60a1 100644 --- a/packages/metro/src/integration_tests/__tests__/server-test.js +++ b/packages/metro/src/integration_tests/__tests__/server-test.js @@ -25,6 +25,14 @@ const fetchAndClose = (path: string) => headers: {Connection: 'close'}, }); +const sourcesOfIndexMap = (indexMap: { + readonly sections: ReadonlyArray<{ + readonly map: {readonly sources: ReadonlyArray, ...}, + ... + }>, + ... +}): Array => indexMap.sections.flatMap(section => section.map.sources); + describe('Metro development server serves bundles via HTTP', () => { let httpServer; const bundlesDownloaded = new Set(); @@ -122,6 +130,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', + ); + expect(directResponse.ok).toBe(true); + const directMap = await directResponse.json(); + const prefixedResponse = await fetchAndClose( + 'http://localhost:' + + httpServer.address().port + + '/[metro-project]/TestBundle.map?platform=ios&dev=true&minify=false', + ); + expect(prefixedResponse.ok).toBe(true); + const prefixedMap = await prefixedResponse.json(); + expect(sourcesOfIndexMap(prefixedMap).sort()).toEqual( + sourcesOfIndexMap(directMap).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', + ); + expect(directResponse.ok).toBe(true); + const directMap = await directResponse.json(); + 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(watchFolderResponse.ok).toBe(true); + const watchFolderMap = await watchFolderResponse.json(); + expect(sourcesOfIndexMap(watchFolderMap).sort()).toEqual( + sourcesOfIndexMap(directMap).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';