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';