diff --git a/packages/cli-server-api/src/statusPageMiddleware.ts b/packages/cli-server-api/src/statusPageMiddleware.ts index b7d690752..2d99ed095 100644 --- a/packages/cli-server-api/src/statusPageMiddleware.ts +++ b/packages/cli-server-api/src/statusPageMiddleware.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ import http from 'http'; +import {normalizeProjectRoot} from '@react-native-community/cli-tools'; /** * Status page so that anyone who needs to can verify that the packager is @@ -16,7 +17,7 @@ export default function statusPageMiddleware( ) { res.setHeader( 'X-React-Native-Project-Root', - new URL(`file:///${process.cwd()}`).pathname.slice(1), + normalizeProjectRoot(process.cwd()), ); res.end('packager-status:running'); } diff --git a/packages/cli-tools/src/__tests__/findDevServerPort.test.ts b/packages/cli-tools/src/__tests__/findDevServerPort.test.ts new file mode 100644 index 000000000..d130c02af --- /dev/null +++ b/packages/cli-tools/src/__tests__/findDevServerPort.test.ts @@ -0,0 +1,69 @@ +import findDevServerPort from '../findDevServerPort'; +import isPackagerRunning from '../isPackagerRunning'; +import handlePortUnavailable from '../handlePortUnavailable'; + +jest.mock('../isPackagerRunning'); +jest.mock('../handlePortUnavailable'); +jest.mock('../port', () => ({ + logAlreadyRunningBundler: jest.fn(), + askForPortChange: jest.fn(), + logChangePortInstructions: jest.fn(), +})); + +const isPackagerRunningMock = isPackagerRunning as jest.MockedFunction< + typeof isPackagerRunning +>; +const handlePortUnavailableMock = handlePortUnavailable as jest.MockedFunction< + typeof handlePortUnavailable +>; + +beforeEach(() => { + jest.clearAllMocks(); + handlePortUnavailableMock.mockResolvedValue({port: 8082, packager: true}); +}); + +test('reuses the running dev server when the project root has a space', async () => { + const root = '/Users/me/my app'; + + isPackagerRunningMock.mockResolvedValue({ + status: 'running', + root: '/Users/me/my%20app', + }); + + await expect(findDevServerPort(8081, root)).resolves.toEqual({ + port: 8081, + startPackager: false, + }); + expect(handlePortUnavailableMock).not.toHaveBeenCalled(); +}); + +test('reuses the running dev server for a plain ASCII project root', async () => { + const root = '/Users/me/project'; + + isPackagerRunningMock.mockResolvedValue({ + status: 'running', + root: '/Users/me/project', + }); + + await expect(findDevServerPort(8081, root)).resolves.toEqual({ + port: 8081, + startPackager: false, + }); + expect(handlePortUnavailableMock).not.toHaveBeenCalled(); +}); + +test('asks for another port when a different project owns the port', async () => { + isPackagerRunningMock.mockResolvedValue({ + status: 'running', + root: '/Users/me/other', + }); + + await expect(findDevServerPort(8081, '/Users/me/project')).resolves.toEqual({ + port: 8082, + startPackager: true, + }); + expect(handlePortUnavailableMock).toHaveBeenCalledWith( + 8081, + '/Users/me/project', + ); +}); diff --git a/packages/cli-tools/src/__tests__/normalizeProjectRoot.test.ts b/packages/cli-tools/src/__tests__/normalizeProjectRoot.test.ts new file mode 100644 index 000000000..4adb27b11 --- /dev/null +++ b/packages/cli-tools/src/__tests__/normalizeProjectRoot.test.ts @@ -0,0 +1,18 @@ +import normalizeProjectRoot from '../normalizeProjectRoot'; + +test('leaves a plain posix path untouched', () => { + expect(normalizeProjectRoot('/Users/me/project')).toBe('/Users/me/project'); +}); + +test('percent-encodes characters that are not valid in a header', () => { + expect(normalizeProjectRoot('/Users/me/my app')).toBe('/Users/me/my%20app'); + expect(normalizeProjectRoot('/привіт/path')).toBe( + '/%D0%BF%D1%80%D0%B8%D0%B2%D1%96%D1%82/path', + ); +}); + +test('converts a windows path to forward slashes', () => { + expect(normalizeProjectRoot('C:\\Users\\me\\project')).toBe( + 'C:/Users/me/project', + ); +}); diff --git a/packages/cli-tools/src/findDevServerPort.ts b/packages/cli-tools/src/findDevServerPort.ts index e3abfe945..0201ffd9d 100644 --- a/packages/cli-tools/src/findDevServerPort.ts +++ b/packages/cli-tools/src/findDevServerPort.ts @@ -1,5 +1,6 @@ import handlePortUnavailable from './handlePortUnavailable'; import isPackagerRunning from './isPackagerRunning'; +import normalizeProjectRoot from './normalizeProjectRoot'; import {logAlreadyRunningBundler} from './port'; const findDevServerPort = async ( @@ -18,7 +19,7 @@ const findDevServerPort = async ( typeof packagerStatus === 'object' && packagerStatus.status === 'running' ) { - if (packagerStatus.root === root) { + if (packagerStatus.root === normalizeProjectRoot(root)) { startPackager = false; logAlreadyRunningBundler(port); } else { diff --git a/packages/cli-tools/src/getNextPort.ts b/packages/cli-tools/src/getNextPort.ts index be4a5a4d0..26320d269 100644 --- a/packages/cli-tools/src/getNextPort.ts +++ b/packages/cli-tools/src/getNextPort.ts @@ -1,4 +1,5 @@ import isPackagerRunning from './isPackagerRunning'; +import normalizeProjectRoot from './normalizeProjectRoot'; type Result = { start: boolean; @@ -19,10 +20,7 @@ const getNextPort = async (port: number, root: string): Promise => { const isRunning = typeof result === 'object' && result.status === 'running'; - if ( - isRunning && - result.root === new URL(`file:///${root}`).pathname.slice(1) - ) { + if (isRunning && result.root === normalizeProjectRoot(root)) { // Found running bundler for this project, so we do not need to start packager! start = false; } else if (isRunning || result === 'unrecognized') { diff --git a/packages/cli-tools/src/index.ts b/packages/cli-tools/src/index.ts index 19132c2aa..7cefb9620 100644 --- a/packages/cli-tools/src/index.ts +++ b/packages/cli-tools/src/index.ts @@ -12,6 +12,7 @@ export * from './prompt'; export * as link from './doclink'; export {default as startServerInNewWindow} from './startServerInNewWindow'; export {default as findDevServerPort} from './findDevServerPort'; +export {default as normalizeProjectRoot} from './normalizeProjectRoot'; export * from './port'; export {default as cacheManager} from './cacheManager'; export {default as runSudo} from './runSudo'; diff --git a/packages/cli-tools/src/normalizeProjectRoot.ts b/packages/cli-tools/src/normalizeProjectRoot.ts new file mode 100644 index 000000000..49813204d --- /dev/null +++ b/packages/cli-tools/src/normalizeProjectRoot.ts @@ -0,0 +1,13 @@ +/** + * Normalizes a project root to the form used by the dev server's + * `X-React-Native-Project-Root` header. + * + * A header value has to be a valid ISO-8859-1 string, so the dev server + * percent-encodes the path before sending it. Anything comparing a local + * project root against that header has to apply the same transformation, + * otherwise roots containing characters that get encoded (a space, an accent, + * a non-latin script) never match. + */ +export default function normalizeProjectRoot(root: string): string { + return new URL(`file:///${root}`).pathname.slice(1); +}